이 글은 인프런 [하루코딩]님의 무료 강의 [Do it! 알고리즘 코딩테스트 with JAVA]를 따라가며,
직접 문제를 풀고 느낀 점, 새롭게 배운 개념 및 깨달음을 정리하는 글입니다.
문제마다 풀이 아이디어와 내가 고민했던 과정과 코드를 집어 넣었습니다.
모든 문제는 저작권 문제로 링크 형식으로 제공합니다.
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
int[] arr = new int[n + 1];
int[] prefixSum = new int[n + 1];
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= n; i++) {
arr[i] = Integer.parseInt(st.nextToken());
prefixSum[i] = prefixSum[i - 1] + arr[i]; // 누적합
}
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int start = Integer.parseInt(st.nextToken());
int end = Integer.parseInt(st.nextToken());
System.out.println(prefixSum[end] - prefixSum[start - 1]);
}
}
입력값과 배열 인덱스를 맞추기 위해 배열을 한 칸 더 크게 만들고, 0번 인덱스를 비워둔 채 1번부터 채워서 인덱스 혼동 없이 구현했다.
처음엔 단순 반복문으로 구간합을 직접 계산하다가 시간 초과가 발생해, 더 효율적인 방법을 고민함.
누적합(구간합) 배열을 사용하면, [start, end] 구간의 합을 prefixSum[end] - prefixSum[start - 1] 한 줄로 빠르게 구할 수 있다는 점화식적 사고로 접근했다.
이런 누적합 방식은 한 번의 전처리(O(N))만 하면, 이후 구간합 쿼리를 O(1)로 처리할 수 있어 효율적임을 알게 됐다.
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
int start = 1, end = 1, sum = 1, count = 0;
while (start <= N) {
if (sum < N) {
end++;
sum += end;
} else if (sum == N) {
count++;
sum -= start;
start++;
} else {
sum -= start;
start++;
}
}
System.out.println(count);
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int m = Integer.parseInt(br.readLine());
String[] s = br.readLine().split(" ");
ArrayList<Integer> numList = new ArrayList<>();
for (String t : s) {
numList.add(Integer.parseInt(t));
}
Collections.sort(numList); // 오름차순 정렬
int count = 0;
int left = 0;
int right = n - 1;
while (left < right) {
int sum = numList.get(left) + numList.get(right);
if (sum == m) {
count++;
left++;
right--;
} else if (sum < m) {
left++;
} else {
right--;
}
}
System.out.println(count);
}
while (start <= N) { if (sum < 목표) { end++; sum += arr[end]; } ... }
while (left < right) { int sum = arr[left] + arr[right]; ... }
문제에서 “연속된 구간/부분합”이냐, “임의의 쌍/조합”이냐를 먼저 구분하면
투포인터의 시작 위치와 이동 방식이 자연스럽게 결정된다!
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int length = Integer.parseInt(st.nextToken()); // DNA 문자열 길이
int answer_length = Integer.parseInt(st.nextToken()); // 비밀번호(부분문자열) 길이
String s = br.readLine();
char[] charArray = s.toCharArray();
st = new StringTokenizer(br.readLine());
// 최소 조건 map
HashMap<Character, Integer> minMap = new HashMap<>();
minMap.put('A',Integer.parseInt(st.nextToken()));
minMap.put('C',Integer.parseInt(st.nextToken()));
minMap.put('G',Integer.parseInt(st.nextToken()));
minMap.put('T',Integer.parseInt(st.nextToken()));
// 윈도우 내 현재 카운트 map
HashMap<Character, Integer> windowMap = new HashMap<>();
windowMap.put('A', 0);
windowMap.put('C', 0);
windowMap.put('G', 0);
windowMap.put('T', 0);
int result = 0;
// 초기 윈도우 세팅
for (int i = 0; i < answer_length; i++) {
char c = charArray[i];
windowMap.put(c, windowMap.get(c) + 1);
}
if (isSatisfied(minMap, windowMap)) result++;
// 슬라이딩 윈도우
for (int i = answer_length; i < length; i++) {
// 윈도우 오른쪽 문자 추가
char in = charArray[i];
windowMap.put(in, windowMap.get(in) + 1);
// 윈도우 왼쪽 문자 제거
char out = charArray[i - answer_length];
windowMap.put(out, windowMap.get(out) - 1);
if (isSatisfied(minMap, windowMap)) result++;
}
System.out.println(result);
}
// 최소 조건을 만족하는지 체크하는 함수
private static boolean isSatisfied(HashMap<Character, Integer> minMap, HashMap<Character, Integer> windowMap) {
for (char c : minMap.keySet()) {
if (windowMap.get(c) < minMap.get(c)) return false;
}
return true;
}
"부분합/카운팅"과 같이 연속된 범위에서 뭔가를 구하는 문제가 나오면 사용
문제를 풀고 개념을 정리하다보니, 문득 드는 생각이 있었다.
'투포인터와 슬라이딩 윈도우.. 너무 비슷한데?' 그래서 비교 해봤다.
투포인터 | 슬라이딩 윈도우 | |
---|---|---|
개념 | 인덱스 두 개로 구간/조합 탐색 | 고정 크기 구간(윈도우) 한 칸씩 이동 |
패턴 | 두 포인터 독립/양방향 이동 | 윈도우 범위 고정, 한쪽만 이동 |
적용 예시 | 연속합/쌍/조합/부분합(2018,1940,2sum) | 구간합/빈도(12891, 부분문자열 카운트 등) |
특징 | 구간 확장·축소/양끝 조합/자유 이동 | 구간 크기 고정/추가·제거만 갱신 |
관계 | 투포인터가 “상위 개념”, 슬라이딩 윈도우는 하위 | 투포인터의 특수형 |
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int length = Integer.parseInt(br.readLine());
int[] arr = new int[length];
for (int i = 0; i < length; i++) {
arr[i] = Integer.parseInt(br.readLine());
}
PriorityQueue<Integer> pq = new PriorityQueue<>(
(a,b) -> {
if (Math.abs(a) == Math.abs(b)) {
return a - b;
} else {
return Math.abs(a) - Math.abs(b);
}
}
);
for (int x : arr) {
if (x == 0) {
if (pq.isEmpty()) {
sb.append(0);
sb.append("\n");
}else{
Integer poll = pq.poll();
sb.append(poll);
sb.append("\n");}
} else {
pq.add(x);
}
}
System.out.println(sb.toString());
}
여기서 추가적으로, 우선순위 큐의 정렬 기준을 직접 커스터마이징 하다보니
Comparable과 Comparator의 차이점도 자연스럽게 알게 되었다.
두 인터페이스의 핵심 차이는 다음과 같다:
public class Student implements Comparable<Student> {
int score;
public Student(int score) { this.score = score; }
@Override
public int compareTo(Student o) {
return this.score - o.score; // 오름차순
}
}
PriorityQueue<Student> pq = new PriorityQueue<>();
)
PriorityQueue는 기본적으로 comparator가 null이기 때문에,
내부적으로 compareTo() 메서드 기준으로 정렬하게 된다.
import java.util.Comparator;
public class ScoreDescComparator implements Comparator<Student> {
@Override
public int compare(Student a, Student b) {
return b.score - a.score; // 내림차순
}
}
PriorityQueue<Student> pq = new PriorityQueue<>(new ScoreDescComparator());
)
PriorityQueue<Student> pq = new PriorityQueue<>(
new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return b.score - a.score; // 내림차순
}
}
);
)
PriorityQueue<Student> pq = new PriorityQueue<>(
(a, b) -> b.score - a.score // 내림차순
);
)
이번 문제를 풀면서 Comparable과 Comparator의 동작 방식을 실제로 체득할 수 있었고,
특히 람다식으로 간단하게 정렬 기준을 작성할 수 있어 실전에도 적극 활용할 수 있을 것 같다.