▶ 1차원 배열에 존재하는 순차적 부분 배열에 접근해야 할 때 두개의 점을 활용하여 중복 연산을 줄이는 알고리즘
예시 문제를 활용하여 알아보자.
문제 : https://www.acmicpc.net/problem/2003
2003번: 수들의 합 2
첫째 줄에 N(1 ≤ N ≤ 10,000), M(1 ≤ M ≤ 300,000,000)이 주어진다. 다음 줄에는 A[1], A[2], …, A[N]이 공백으로 분리되어 주어진다. 각각의 A[x]는 30,000을 넘지 않는 자연수이다.
www.acmicpc.net
연속되는 부분수열의 합이 M인 경우를 찾는 문제이다.
예제 입력 2의
1 2 3 4 2 5 3 1 1 2 의 경우를 살펴보면
1 | 2 | 3 | 4 | 2 | 5 | 3 | 1 | 1 | 2 |
1 |
연속되는 합이 5가 되는 경우를 찾기 위해
첫 번째 원소의 합 = 1
1 | 2 | 3 | 4 | 2 | 5 | 3 | 1 | 1 | 2 |
1 | 3 |
1 + 2 번째의 원소의 합 = 3
1 | 2 | 3 | 4 | 2 | 5 | 3 | 1 | 1 | 2 |
1 | 3 | 6 |
1 + 2 + 3번째의 원소의 합 = 6 이고, 나머지 숫자는 자연수이므로
더이상 만족할 수 없기 때문에 반복을 중지하고,
이제 두번째 원소부터 위의 연산을 반복할 것이다.
하지만 위 방법은 첫 번째 탐색시 방문한 1,2,3을 두 번째 탐색 시 2,3 중복 방문하므로 수가 길어질수록 매우 비효율적이 될 것이다.
이와 같은 상황에 투 포인터 알고리즘을 사용할 수 있다.
배열의 위치를 가르키는 Left, Right 라는 두 개의 포인터를 사용한다.
초기 상태에선 둘 다 arr[0] 값을 가르키고 있다.
이제 찾고자 하는 숫자가 M 이라고 할때
위 규칙에 따라 연산을 진행한다.
1은 M보다 작으므로 R이 한 칸 이동한다.
3은 M보다 작으므로 R이 한 칸 이동한다.
6은 M보다 크므로 L이 한 칸 이동한다.
현재 정답을 찾았으므로 정답 카운트를 +1 해주고
다시 탐색하기 위하여 L도 한 칸 이동한다.
위와 같은 작업을 배열의 마지막까지 반복하면 O(n) 의 시간복잡도로 결과를 얻을 수 있다.
이제 이 과정을 코드로 나타내보자.
// [백준] 2003. 수들의 합 2 (Java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
static int[] arr;
static int n;
static int m;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
n = Integer.parseInt(st.nextToken());
m = Integer.parseInt(st.nextToken());
arr = new int[n];
st = new StringTokenizer(br.readLine());
for(int i = 0; i < n; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
System.out.println(twoPointer());
}
public static int twoPointer() {
int left = 0;
int right = 0;
int sum = 0;
int count = 0;
while (true) {
// 1 + 3. L ~ R 까지의 부분배열의 합이 M 보다 크거나 같으면 L + 1
if(sum >= m) {
sum -= arr[left++];
}
// 종료 조건 (right이 마지막 범위를 넘어갔을 경우)
else if(right == n) {
break;
}
// 2. L ~ R 까지의 부분배열의 합이 M 보다 작으면 R + 1
else if(sum < m) {
sum += arr[right++];
}
// 3.L ~ R 까지의 부분배열의 합이 M 이면 결과 카운트 + 1
if(sum == m){
count++;
}
}
return count;
}
}
위에서 살펴본 것 같이, 투 포인터 알고리즘은 부분 배열의 길이가 가변적인 것을 알 수 있다.
문제에서 부분 배열의 길이가 일정하다면
투 포인터 알고리즘과 유사한 슬라이딩 윈도우 알고리즘을 사용할 수 있다.
▶ 투 포인터와 유사하게 부분 배열에 효율적으로 접근하기 위한 알고리즘. (부분 배열 길이 고정)
위와 같이 창문을 오른쪽으로 쭉 밀면서 안에 들어오는 배열을 탐색한다고 생각하면 된다.
길이가 고정적이므로 투 포인터와 다르게 하나의 포인터만 있으면 된다.
예시 문제를 활용하여 알아보자.
문제 : https://www.acmicpc.net/problem/2559
2559번: 수열
첫째 줄에는 두 개의 정수 N과 K가 한 개의 공백을 사이에 두고 순서대로 주어진다. 첫 번째 정수 N은 온도를 측정한 전체 날짜의 수이다. N은 2 이상 100,000 이하이다. 두 번째 정수 K는 합을 구하기
www.acmicpc.net
특정 길이의 부분 배열들의 합을 각각 구해서, 그 중 가장 큰 값을 찾아야 하는 문제이다.
위 문제도 일반적인 탐색 방법을 사용할 경우 매우 비효율적이라는 것을 위의 투 포인터 때의 예시에서 알 수 있다.
예제 입력 1의
주어진 배열 : 3 -2 -4 -9 0 3 7 13 8 -3
배열의 길이 : 2
의 경우를 살펴보면
위와 같이 한칸씩 길이가 2인 창문을 밀어가며 확인하면서
결과적으로 문제에서 요구하는 결과를 O(n) 의 시간복잡도로 빠르게 얻을 수 있다.
이 과정도 코드로 나타내보자.
// [백준] 2559. 수열 (Java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
static int n;
static int k;
static int[] arr;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
n = Integer.parseInt(st.nextToken());
k = Integer.parseInt(st.nextToken());
arr = new int[n];
st = new StringTokenizer(br.readLine());
for(int i = 0; i < n; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
System.out.println(slidingWindow());
}
public static int slidingWindow() {
int max = 0;
int sum = 0;
for(int i = 0; i < n; i++) {
sum += arr[i];
// 최초에 나온 합을 최댓값으로 잡아놓음
if(i == k - 1) {
max = sum;
}
// 처음 길이를 벗어났을 때 부터 한칸씩 밀어주면서 최댓값 비교
if(i >= k) {
sum -= arr[i - k];
max = Math.max(max, sum);
}
}
return max;
}
}
내 블로그 - 관리자 홈 전환 |
Q
Q
|
---|---|
새 글 쓰기 |
W
W
|
글 수정 (권한 있는 경우) |
E
E
|
---|---|
댓글 영역으로 이동 |
C
C
|
이 페이지의 URL 복사 |
S
S
|
---|---|
맨 위로 이동 |
T
T
|
티스토리 홈 이동 |
H
H
|
단축키 안내 |
Shift + /
⇧ + /
|
* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.