브루트 포스(brute force)
이러한 브루트 포스방법 중에서 투포인터, 중간에서 만나기에 관해서 소개를 하려고 한다.
두 포인터(Two Pointers)
배열에서 전체의 경우의 수를 반복문을 통해서 찾는 것이 아니라 두개의 포인터를 조작하여 원하는 결과를 얻는 방법이다.
여기서 두개의 포인터를 이용하여 기존보다 시간복잡도가 줄어드는 구조를 만들 수 있다.
두 포인터 예제
2003 - 수들의 합2 https://www.acmicpc.net/problem/2003
A[i]+...+A[j] = M이 되는 i,j 쌍의 개수를 찾는 문제와 같다.
가장 기본적으로 생각 할 수 있는 방법은 반복문을 만드는 것이다.
반복문으로 i와 j를 선택하고 i부터 j까지의 합을 구한다.
(i선택 x j선택 x 합더하기 = O(N^3))
이 문제의 N의 범위는 10^4이다. 고로 N^3 은 10^12으로 1조로, 시간제한인 0.5초를 통화하지 못한다.
//5% 시간초과
#include <iostream>
#include <vector>
using namespace std;
int main(void) {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
long long n, m;
int ans = 0;
cin >> n >> m;
vector<int> v(n);
for (int i = 0; i < n; ++i) {
cin >> v[i];
}
for (int i = 0; i < n; ++i) {
for (int j = n - 1; j >= i; --j) {
long long sum = 0;
for (int k = i; k <= j; ++k) {
sum += v[k];
}
if (sum == m) ans++;
}
}
cout << ans;
}
이때 생각 할 수 있는 방법은 투포인터 이다.
위의 문제에서 합은 변하지 않는데 여러 번 구하는 것은 투포인터와 중복된 연산으로 없앨 수 있다.
start, end포인터로 연속한 부분수열의 첫과 끝을 잡는다.
이때 합이 목표보다 작을 경우 end+1
합이 목표보다 클 경우 start +1
합이 목표와 같을 경우 start +1, end+1
여기서 중요한 부분은 각 포인터가 한방향으로만 이동하기 때문에 연속한 부분 수열을 선택하는 경우의 수가 O(N^2)가 아닌 O(N)이라는 점이다.
#include <iostream>
#include <vector>
using namespace std;
int main(void) {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> v(n);
for (int i = 0; i < n; ++i) {
cin >> v[i];
}
int start = 0, end = 0, sum = 0, ans = 0;
while (true) {
if (sum < m) {
if (end >= n) break;
sum += v[end++];
}
else if (sum > m) {
sum -= v[start++];
}else{
ans++;
if (end >= n) break;
sum -= v[start++];
sum += v[end++];
}
}
cout << ans;
}
중간에서 만나기(Meet in the Middle, MITM)
문제를 절반으로 나눠서 양쪽 절반에서 모든 경우를 다 해보는 방법이다.
시간복잡도 즉, 탐색의 크기를 많이 줄일 수 있다.
중간에서 만나기 예제
1208 - 부분수열의 합2 https://www.acmicpc.net/problem/1208
가장 먼저 생각해 볼 수 있는 방법은 모든 부분수열을 만들어서 확인해 보는 것이다.
부분수열의 갯수는 2^N이므로 (각 수에 대해서 뽑거나 안뽑거나 2가지 경우, dfs 등으로 구현가능)
하지만 n이 40이므로 2^40 약 10^12으로 1000초 이상 걸리므로 시간제한에 걸린다.
그럼 중간에서 만나기를 활용해보자.
수열을 두개로 쪼개서 부분수열의 합을 구하고 정렬하자.
이때 n을 반으로 줄인다면 2^20 약 10^6이므로 시간제한에 걸리지 않는다.
(정렬은 M개의 원소에 대해서 O(MlogM))
그 후에 두 포인터를 이용해서 갯수를 세보자.
N개의 수를 이용해서 전체 부분수열을 만들었을때는 포인터가 하나였지만
두개로 쪼개니 포인터가 두개다. (물론 시간 복잡도도 많이 작아졌다.)
양쪽을 선택해서 합을 확인 하며 된다.
//한쪽 배열에서 만든 부분수열의 합이 같은것이 여러개 있을 경우에는 곱해서 더해야 한다.
// 목표 합이 0일 경우에 0 과 0을 선택 하는 경우는 빼야 한다. 공집합은 미포함.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int n, s;
vector<int> v;
vector<int> v1, v2;
int temp;
void dfs(int index, int sum, int end, vector<int> &a) {
if (index == end) {
a.push_back(sum);
return;
}
dfs(index + 1, sum, end, a);
dfs(index + 1, sum + v[index], end, a);
}
long long moveRight(vector<int> &a, int index) { //같은 수가 몇개인지 리턴
long long ans = 1;
if (index == a.size() - 1) return ans;
while (a[index] == a[index + 1]) {
ans++;
index++;
if (index == a.size() - 1) return ans;
}
return ans;
}
int main(void) {
cin.tie(nullptr);
cout.tie(nullptr);
ios_base::sync_with_stdio(false);
cin >> n >> s;
for (int i = 0; i < n; ++i) {
int num;
cin >> num;
v.push_back(num);
}
//부분수열의 합을 둘로 나눠서 각각 구한후 정렬.
dfs(0, 0, n / 2, v1);
dfs(n / 2, 0, n, v2);
long long ans = 0;
int start1 = 0;
int start2 = 0;
sort(v1.begin(), v1.end());
sort(v2.begin(), v2.end());
reverse(v2.begin(), v2.end());
while (start1 < v1.size() && start2 < v2.size()) {
int sum = v1[start1] + v2[start2];
if (sum == s) {
long long s1 = moveRight(v1, start1);
long long s2 = moveRight(v2, start2);
start1 += s1;
start2 += s2;
ans += s1 * s2; //같은 수가 여러개 일경우 곱
}
else if (sum > s) {
long long s2 = moveRight(v2, start2);
start2 += s2;
}
else {
long long s1 = moveRight(v1, start1);
start1 += s1;
}
}
if (s == 0) ans--;
cout << ans;
}
'Algorithm > Intermediate' 카테고리의 다른 글
유니온 파인드[Union Find] (0) | 2021.10.02 |
---|---|
스택 중간난이도 문제 풀이(Stack PS) (0) | 2021.10.01 |
이분탐색 문제 풀이[Binary Seach PS] (0) | 2021.08.24 |
분할 정복법(Divide and Conquer)(이분 탐색, 퀵 소트, 퀵 셀렉트, 머지 소트(병합 정렬))(Binary Search, Quick Sort, Quick Select, Merge Sort) (2) | 2021.08.05 |
그리디 알고리즘(Greedy Algorithm) (0) | 2021.06.12 |