안녕하세요. 이번 포스팅에서는 가장 긴 증가하는 부분 수열문제를 해결해 보도록 하겠습니다.
우선 가장 기본적인 포맷은 다음과 같습니다.
https://www.acmicpc.net/problem/11053
11053번: 가장 긴 증가하는 부분 수열
수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이
www.acmicpc.net
이 문제를 dp로 해결할 수도 있는데, 이번 포스팅에서는 dp를 사용하지 않고 풀어보도록 하겠습니다.
#include<bits/stdc++.h>
using namespace std;
int n, res;
int lis[1000+1];
int main(){
ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
cin >> n;
vector<int> v(n);
for(int i = 0; i < n; i++) cin >> v[i];
for(int i = 0; i < n; i++) {
int maxVal = 0;
for(int j = 0; j < i; j++) {
if(v[j] < v[i] && lis[j] > maxVal) {
maxVal = lis[j];
}
}
lis[i] = maxVal+1;
res = max(res, lis[i]);
}
cout << res << endl;
return 0;
}
이 문제의 경우 시간제한 1초에 N의 제한이 1000까지이므로 N^2의 알고리즘도 충분히 사용 가능합니다.
이중 포문을 돌면서, 현재 j가 가리키는 인덱스가 i를 가리키는 인덱스보다 작고, (즉 이전의 부분수열중 일부가 되고)
j의 lis배열 (j까지의 최대 증가 수열의 수)보다 지금까지 센 count가 적다면,
Count를 lis[j]로 변경해주시면 됩니다. 그리고 j의 루프가 끝났다면 lis[i]에 이전까지의 최대 증가수열의 count 수 + 1을 갱신해주면 됩니다.
코드로 보았을 때 직관적으로 이해하기 어려운 부분은 없습니다.
https://www.acmicpc.net/problem/14002
14002번: 가장 긴 증가하는 부분 수열 4
수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이
www.acmicpc.net
그 다음 단계로 이 문제를 해결해보도록 하겠습니다.
이 문제의 경우 위 문제와 제한은 똑같지만, 가장 긴 증가하는 부분 수열의 경로를 출력해야 합니다.
#include<bits/stdc++.h>
using namespace std;
int n, start, res;
int lis[1000+1];
int trace[1000+1];
int main(){
ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
cin >> n;
vector<int> v(n);
for(int i = 0; i < n; i++) cin >> v[i];
fill(trace, trace+1001, -1);
for(int i = 0; i < n; i++) {
int maxVal = 0;
for(int j = 0; j < i; j++) {
if(v[j] < v[i] && lis[j] > maxVal) {
maxVal = lis[j];
trace[i] = j;
}
}
lis[i] = maxVal+1;
if(res < lis[i]) {
res = lis[i], start = i;
}
}
vector<int> path;
while(trace[start] != -1) {
path.push_back(start);
start = trace[start];
}
path.push_back(start);
cout << res << "\n";
for(int i = path.size()-1; i >= 0; i--) cout << v[path[i]] << " ";
return 0;
}
기본적인 알고리즘은 처음 문제와 같습니다. 하지만 이 문제의 경우 지나온 경로를 출력해야 합니다. 따라서 maxVal만 갱신하는 것이 아니라,
maxVal이 갱신된다면, 현재를 기준으로 가장 긴 증가하는 부분 수열의 전 단계는 j이므로 trace[i] = j
즉 i의 전단계는 j이다 라는 사실을 기록해주면 됩니다.
그리고 이제 마지막으로 res가 갱신되는 순간의 끝점부터 시작하여 다시 처음으로 회귀하면 됩니다. 저는 처음에 배열을 -1로 초기화하여, 배열의 값이 -1이 아닌 동안 계속 while loop로 지나온 path를 추가해 주었고, 마지막에 -1인 시작점을 추가해 주었습니다.
마지막으로
https://www.acmicpc.net/problem/14003
14003번: 가장 긴 증가하는 부분 수열 5
첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)
www.acmicpc.net
가장 긴 증가하는 부분 수열 5를 보도록 하겠습니다.
문제에서 요구하는 바는 같습니다. 하지만 범위가 1000000까지 늘어났습니다.
범위가 1000000이면 죽었다 깨어나도 N^2의 알고리즘으로는 해결할 수 없습니다. 따라서 저희는 NlogN 이하의 알고리즘을 찾아야합니다.
우선 이 문제를 해결하기 위해 기본적인 이분 탐색과 lower_bound를 이해하고 계시면 좋습니다.
https://chanho0912.tistory.com/13?category=871243
[C++/Algorithm] upper_bound, lower_bound 이해하기
2021년 상반기 카카오 인턴 코딩 테스트에 응시했었는데, 당시 3번 문제에 upper_bound, lower_bound를 사용했었다. 그 당시에 조금 헤맸던 기억이 있어서 이번 기회에 한번 정리해보려 한다. 우선 c++ refe
chanho0912.tistory.com
lower_bound를 활용하여 결과 배열을 계속 만들어간다고 생각하시면 됩니다.
#include<bits/stdc++.h>
using namespace std;
int n, MINVAL = -1000000001;
int main(){
ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
cin >> n;
int len = 0;
vector<int> v(n), lis(n, MINVAL), res;
vector<pair<int,int>> ans(n);
for(int i = 0; i < n; i++) {
cin >> v[i];
auto lower_pos = lower_bound(lis.begin(), lis.begin()+len, v[i]);
int lower_posIdx = (int)(lower_pos-lis.begin());
if(*lower_pos == MINVAL) len++;
*lower_pos = v[i];
ans[i].first = lower_posIdx;
ans[i].second = v[i];
}
cout << len << endl;
for(int i = n-1; i >= 0; i--) {
if(ans[i].first == len-1) {
res.push_back(ans[i].second);
len--;
}
}
reverse(res.begin(), res.end());
for(auto a : res) cout << a << " ";
return 0;
}
만약에 input으로
6
10 20 10 30 20 50
이 입력되었다고 생각해볼게요. 그러면 초기 배열은 MINVAL로 초기화되어있습니다. 그리고 len이 0입니다. 따라서 lower_bound(lis.begin(), lis.begin()+len, v[i])를 수행하였을 때 v[i]의 값을 찾지 못하고 MINVAL을 반환하게 됩니다.
저희는 len을 1 증가시키고 (결과 배열의 길이가 1 증가됨)
결과 배열에 10을 넣습니다.
같은 방식으로 20도 넣게 됩니다.
이 상황에 10이 들어오게 되면, 결과 배열에 10이 있는 인덱스의 위치를 반환합니다. 이를 10으로 대체합니다.
이 방식으로 진행하면
10
10 20
10 20
10 20 30
10 20 30
10 20 30 50
의 순으로 결과 배열이 바뀌게 됩니다. 하지만 이 방법만 사용하게 되면,
4
1 5 10 4
의 반례를 만나게 됩니다. 이 경우 1 4 10이 결과로 반환되는데, 4는 10 이후의 원소이므로 오답입니다. 이를 해결하기 위해서 인덱스의 정보도 같이 저장한 뒤, len -1부터 역행하며 찾아가면서 값을 찾았다면, 그 이전의 값만을 대상으로 결과에 추가해주시면 됩니다.
*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.
'Algorithm > Algorithm Logic' 카테고리의 다른 글
[Algorithm] DFS, BFS 정리 (0) | 2021.07.12 |
---|---|
[C++/Algorithm] 투 포인터, 구간 합 이해하기 (0) | 2021.07.05 |
[C++/Algorithm] 정렬 알고리즘 이해하고 구현하기 (Merge, Quick) #2 (0) | 2021.06.27 |
[C++/Algorithm] 정렬 알고리즘 이해하고 구현하기 (bubble, insertion, selection) #1 (0) | 2021.06.27 |
[C++/Algorithm] 순열과 조합 구현하기 (0) | 2021.06.23 |