Prefix Sum

Prefix sum이란 해당 인덱스까지의 value 들의 누적 합을 말한다.

Index 0 1 2 3 4 5 6
Value   3 5 -2 -1 4 -10
Prefix Sum 0 3 8 6 5 9 -1

위에서 예를 들면 1번 인덱스까지의 누적합은 3이니까 prefix sum이 3이고, 5번 인덱스까지의 누적합은 9이므로 prefix sum은 9이다. 이걸 이용하면 index 1, 5의 prefix sum을 알 때 index 2~4의 value의 합을 구할 수 있다.

Prefix sum을 활용하는 문제를 풀어보자.

Codeforces 466C: Number of Ways

문제

배열의 모든 element를 세 개의 연속된 부분으로 분할하고 각 element의 합이 같도록 하는 방법의 수를 구하는 문제. 즉 sigma(1, i-1, ak) = sigma(i, j, ak) = sigma(j+1, n)을 만족시키는 (i, j)의 순서쌍의 개수를 구해야 한다. 이때 2 ≤ i ≤ j ≤ n - 1이므로 각

처음에 내가 짠 코드. N의 최댓값이 500만이기 때문에 딱 봐도 시간초과가 날 것 같지만 그래도 재귀로 짜봤다. prefix sum을 활용할 생각은 하지도 않고 신나서 풀음..

#include <iostream>
#include <vector>
using namespace std;

long long N, total, cnt; // N의 범위가 10^5이므로 전체 방법의 수가 int의 범위를 넘어갈 수 있음
vector<int> v;

void partition(int start, int sum, int parts) {
	int tmp, i;
	if (parts == 1) { // 세 덩어리로 partitioning이 완료되었으므로 cnt 증가시킴
		cnt++;
		return;
	}
	tmp = 0;
	for (i = start; i < N - (parts - 1); i++) {
		tmp += v[i];
		if (tmp == sum) partition(i + 1, sum, parts-1);
	}
}

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	
	int i, tmp;
	
	cin >> N;
	for (i = 0; i < N; i++) {
		cin >> tmp;
		v.push_back(tmp);
		total += tmp;
	}
	
	if (total % 3 != 0) cout << 0 << endl; // 전체 합이 3의 배수가 아니면 세 개의 subarray로 partitioning 할 수 없다.
	else {
		partition(0, total / 3, 3); 
		cout << cnt << endl;
	}
	return 0;
}

친절한 코드포스는 백준과는 달리 테케를 모두 공개하므로 N이 백만일 때 시간초과가 발생한다는 것을 알게 되었다.

교수님의 코드를 참고해 prefix sum을 이용하여 해당 인덱스까지의 누적합을 prefix[]에 저장하는 방식으로 작성한 코드. 각 element의 범위가 백만 이하이므로 p를 int가 아닌 long long 타입의 벡터로 선언해야 함.

#include <iostream>
#include <vector>
#define MAX 500001
using namespace std;

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(0);

	long long N, total, cnt, cnt3, i;
	vector<long long> v(MAX); // value
	vector<long long> p(MAX); // prefix sum

	p[0] = 0;
	v[0] = 0;

	cin >> N;
	for (i = 1; i <= N; i++) {
		cin >> v[i];
		p[i] = p[i - 1] + v[i];
	}
	
	total = p[N];

	if (total % 3 != 0) cout << 0 << endl;
	else {
		cnt = 0;
		cnt3 = 0;
		if (p[1] == total / 3) cnt3++;

		for (i = 2; i <= N - 1; i++) { // 마지막 partition에도 최소 1개의 element가 있어야 하기 때문에
			if (p[i] == (total / 3) * 2) cnt += cnt3; // 현재 보고있는게 전체의 2/3이면 지금껏 본 1/3의 개수를 더한다.
			if (p[i] == total / 3) cnt3++;
		}
		cout << cnt << endl;
	}
	return 0;
}

Codeforces 1003C: Intense Heat

문제

prefix sum을 안 썼으면 매번 sum을 구하느라 O(N)이 더 곱해졌을거다. 이래서 많이 아는게 중요함.

오랜만에 소숫점 계산했다..

#include <iostream>
#include <vector>
#include <algorithm> // max
#define MAX 5001
using namespace std;

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(0);

	int N, K, i, j;
	double answer, tmp;
	vector<int> v(MAX);
	vector<int> p(MAX);

	cin >> N >> K;
	p[0] = v[0] = 0;
	for (i = 1; i <= N; i++) {
		cin >> v[i];
		p[i] = p[i - 1] + v[i];
	}

	answer = 0.0;

	for (i = K; i <= N; i++) { // not less than K consecutive days
		for (j = 1; j <= N - (i - 1); j++) {
			tmp = ((double)p[j + i - 1] - p[j - 1]) / (double)i;
			answer = max(tmp, answer);
		}
	}
	printf("%.15f\n", answer);
	return 0;
}

Codeforces 1285B: Just Eat It!

문제

쉽네~하며 풀었다가 시간초과난 문제. O(N^2)에 풀었는데 틀린 테케를 보니 O(N)에 풀어야 하는 것 같다.

이렇게 풀면 시간초과 뜸.

#include <iostream>
#include <vector>
#include <algorithm> // max, min
#define MAX 100001
using namespace std;

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(0);

	int T, N, i, j;
	long long total, partial, tmp;
	cin >> T;

	while (T--) {
		cin >> N;
		vector<int> v(MAX);
		vector<long long> p(MAX);
		partial = 0;

		p[0] = 0;
		for (i = 1; i <= N; i++) {
			cin >> v[i];
			p[i] = p[i - 1] + v[i];
		}
		
		total = p[N];
		
		for (i = 1; i <= N - 1; i++) { // size of the segment
			for (j = 1; j <= N - (i - 1); j++) {
				tmp = p[j + i - 1] - p[j - 1];
				partial = max(partial, tmp);
			}
		}
		if (total > partial) cout << "YES" << endl;
		else cout << "NO" << endl;
	}
	return 0;
}

v의 앞뒤 일부를 지워 total 이상으로 만드는 방법이 존재한다면 NO를 출력하면 된다.

교수님의 코드는 아직도 이해하지 못했지만 나름 풀어봤다.

예를 들어 v[] = {1, -5, 4, -8, 7}의 배열이 있다고 하자. prefix sum과 suffix sum(뒤에서부터 거꾸로인 prefix sum이라 생각하면 됨)을 구해보면 다음과 같다.

Index 0 1 2 3 4 5
Value   1 -5 4 -8 7
Prefix Sum   1 -4 0 -8 -1
Suffix Sum   -1 -2 3 -1 7

이제 마치 퀵소트를 할 때 양 끝에서부터 움직이는 것처럼 prefix sum과 suffix sum 배열의 값을 양 끝에서부터 관찰하자.

i가 2일 때

1

i=2일 때 prefix sum값이 -4로 0보다 작다. prefix sum[2]은 v[1]과 v[2]의 합이고, 이는 v[1] + v[2]를 더한 값은 전체 배열의 합을 오히려 작게 만든다는 것을 의미한다.

실제로 v에서 1~5번 인덱스를 다 더한 값은 1+(-5)+4+(-8)+7 = -1이지만, 1번과 2번 인덱스를 제외하고 더한 값은 4+(-8)+7 = 3이다. 그렇기 때문에 우리는 prefix sum을 사용해 차라리 없는게 이득인 부분, 즉 prefix sum이 양수가 아닐 때가 존재하는지를 찾는다.

만약 prefix sum이 0보다 같거나 작은 지점이 하나라도 있다고 하자. 그러면 Adel은 이 부분을 버릴 것이고 그러면 Yasser의 tastiness(total값)보다 크거나 같은 tastiness 값을 가지게 되므로 프로그램은 NO를 출력할 것이다.

마찬가지로 suffix sum을 활용해 동일한 루틴을 v의 맨 끝에서부터 수행한다.

이렇게 해도 시간초과가 뜨는데, prefix sum나 suffix sum에서 0 이하의 값을 발견하자마자 NO를 출력하도록 코드를 작성하자. flag 값이 한 번 false가 되었으면 나머지는 더 이상 비교할 필요가 없다.

#include <iostream>
#include <vector>
#include <algorithm>
#define MAX 100002
using namespace std;

int N;
vector<int> v(MAX);
vector<long long> p(MAX); // prefix sum
vector<long long> s(MAX); // suffix sum

// key: 앞에서부터랑 뒤에서부터 각각 더해봄. 그 값이 0보다 작으면 NO임! 왜냐하면 그 값이 없었더라면 total보다 큰 값이 나왔을 테니깐~
bool check() {
	int i;
	if (v[1] <= 0 || v[N] <= 0) return false;
	for (i = 1; i <= N; i++) {
		if (p[i] <= 0 || s[N - i + 1] <= 0) return false;
	}
	return true;
}

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(0);

	int T, i;
	cin >> T;

	while (T--) {
		cin >> N;

		p[0] = v[0] = 0;
		for (i = 1; i <= N; i++) {
			cin >> v[i];
			p[i] = p[i - 1] + v[i];
		}

		s[N + 1] = 0;
		for (i = N; i >= 1; i--) s[i] = s[i + 1] + v[i];

		if (check()) printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}