문제 : 외계 신호 분석 (난이도 : 中)
분석
이 문제의 핵심은 입력단의 크기이다. 모든 입력단을 처리하기에는 너무 큰 것을 알 수 있다.
따라서 이 문제는 온라인 알고리즘(online algorithm)을 사용해야 한다.
온라인 알고리즘(online algorithm):
전체 입력이 한 번에 주어지지 않아도 계산을 할 수 있는 알고리즘을 말한다.
예를 들어 삽입 정렬은 새 원소를 이전의 정렬된 목록에 끼워 넣는 방법으로 동작하는데, 처음에 모든 원소가 주어지지 않아도 정렬을 시작할 수 있다.
오프라인 알고리즘(ofline algorithm):
반면 선택 정렬은 남아 있는 모든 원소 중에서 최소의 원소를 찾아서 맨 앞에 옮기는 방식으로 동작하므로, 모든 원소를 알고 있어야 한다.
일단 입력단의 크기를 무시하고 오프라인 알고리즘으로 설계해보자.
그러면 간단히 이중 for문으로 설계할 수 있을 것이다.
// 오프라인 알고리즘 구성
int simple(const vector<int>& signals, int k)
{
int ret = 0;
for (int head = 0; head < signals.size(); head++)
{
int sum = 0;
for (int tail = head; tail < signals.size(); tail++)
{
// sum은 [head, tail] 구간의 합
sum += signals[tail];
if (sum == k) ret++;
if (sum >= k) break;
}
}
return ret;
}
시간 복잡도를 계산해보면 head에 대한 for문 N번 수행되고, tail에 대한 for문은 최대 min(N, K)번 수행될 수 있다.
따라서 O(NK) 로 가정할 수 있다.
여기서 간단한 방법으로 최적화를 진행할 수 있다.
head가 늘어났을 때 tail이 줄어드는 일이 없다.
만약 head가 늘어났는데 tail이 줄어든다면 이 후보 구간은 이전 구간의 부분 구간이 된다.
따라서 tail을 head위치가 아닌 전 위치부터 시작하면 된다.
// 최적화
int optimized(const vector<int>& signals, int k)
{
int ret = 0, tail = 0, rangeSum = signals[0];
for (int head = 0; head < signals.size(); head++)
{
// rangSum이 k 이상인 최초 구간 만날때까지 tail을 옮긴다.
while (rangeSum < k && tail + 1 < signals.size())
rangeSum += signals[++tail];
if (rangeSum == k) ret++;
// signals[head]는 구간에서 빠진다.
rangeSum -= signals[head];
}
return ret;
}
위 알고리즘의 시간은 tail이 0~N까지 증가하므로 분할 상환 분석을 이용하면 O(N) 인 것을 알 수 있다.
이제 이것을 온라인 알고리즘으로 바꿔보자.
head가 증가하고 나면 지나쳐온 숫자들은 저장할 필요가 없다. 우리가 메모리에 저장해야 하는 것은 후보 구간에 포함된 숫자들 뿐이다.
// 온라인 알고리즘
int countRange(int k, int n)
{
RNG rng;
queue<int> range; // 현 구간 내에 숫자들을 저장하는 큐
int ret = 0, rangeSum = 0;
for (int i = 0; i < n; i++)
{
// 구간 내 숫자 추가
int newSginal = rng.next();
rangeSum += newSginal;
range.push(newSginal);
// 구간의 합이 k를 초과하면 구간에서 수를 뺀다.
while (rangeSum > k)
{
rangeSum -= range.front();
range.pop();
}
if (rangeSum == k) ret++;
}
return ret;
}
구현
※ 선형 합동 난수 생성기(linear congruential random number generator)의 구현을 유의 깊게 봐보자.
// 선형 합동 난수 생성기
struct RNG
{
unsigned seed;
RNG() : seed(1983) {}
unsigned next() {
unsigned ret = seed;
seed = ((seed * 214013u) + 2531011u);
return ret % 10000 + 1;
}
};
// 오프라인 알고리즘
int simple(const vector<int>& signals, int k)
{
int ret = 0;
for (int head = 0; head < signals.size(); head++)
{
int sum = 0;
for (int tail = head; tail < signals.size(); tail++)
{
// sum은 [head, tail] 구간의 합
sum += signals[tail];
if (sum == k) ret++;
if (sum >= k) break;
}
}
return ret;
}
// 최적화
int optimized(const vector<int>& signals, int k)
{
int ret = 0, tail = 0, rangeSum = signals[0];
for (int head = 0; head < signals.size(); head++)
{
// rangSum이 k 이상인 최초 구간 만날때까지 tail을 옮긴다.
while (rangeSum < k && tail + 1 < signals.size())
rangeSum += signals[++tail];
if (rangeSum == k) ret++;
// signals[head]는 구간에서 빠진다.
rangeSum -= signals[head];
}
return ret;
}
// 온라인 알고리즘
int countRange(int k, int n)
{
RNG rng;
queue<int> range; // 현 구간 내에 숫자들을 저장하는 큐
int ret = 0, rangeSum = 0;
for (int i = 0; i < n; i++)
{
// 구간 내 숫자 추가
int newSginal = rng.next();
rangeSum += newSginal;
range.push(newSginal);
// 구간의 합이 k를 초과하면 구간에서 수를 뺀다.
while (rangeSum > k)
{
rangeSum -= range.front();
range.pop();
}
if (rangeSum == k) ret++;
}
return ret;
}
int main()
{
int c;
cin >> c;
while (c--)
{
int n, k;
cin >> k >> n;
cout << countRange(k, n) << '\n';
}
return 0;
}
'알고리즘 > 알고리즘 문제 해결 전략' 카테고리의 다른 글
[알고스팟] - 요새 (0) | 2021.01.16 |
---|---|
알고스팟 - 트리 순회 순서 변경 (0) | 2021.01.10 |
스택 - 1. 짝이 맞지 않은 괄호 (0) | 2021.01.08 |
연결 리스트 - 조세푸스 문제 (0) | 2021.01.07 |
동적 계획법 - 13. 두니발 박사의 탈옥 (0) | 2021.01.06 |