반응형

문제링크 : https://www.acmicpc.net/problem/1912

 

1912번: 연속합

첫째 줄에 정수 n(1 ≤ n ≤ 100,000)이 주어지고 둘째 줄에는 n개의 정수로 이루어진 수열이 주어진다. 수는 -1,000보다 크거나 같고, 1,000보다 작거나 같은 정수이다.

www.acmicpc.net

이 문제를 보고 '그냥 모든 경우의 수를 다 구하면 안 되나?'란 생각이 들었다. 이 때의 시간 복잡도는 O(n^2)인데 n의 최댓값이 100,000이라 이 정도면 가능하지 싶었다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int[] seq = new int[n];
        int[] sums = new int[n];
        int max = Integer.MIN_VALUE;
        int sum = 0;
        
        for(int i = 0; i < n; i++) {
            seq[i] = Integer.parseInt(st.nextToken());
            sum += seq[i];
            sums[i] = sum;
            
            if(max < sums[i]) {
                max = sums[i];
            } 
        }
        
        for(int i = 1; i < n; i++) {
            for(int j = 0; j < i; j++) {
                int tmp = sums[i] - sums[j];
                
                if(max < tmp) {
                    max = tmp;
                }
            }
        }
        
        System.out.println(max);
    }
}
 
 
 

 

맞긴 맞았는데 상위권 풀이와 비교해보니 시간 차이가 너무 많이났다-_- 이거이거 잘못 풀었구나...ㅠㅠ

 

제대로 푼 방법에 대한 설명을 하겠다. 0부터 n - 1까지 i를 증가시키면서 그 시점에서의 최댓값을 구한다. 그 시점에서의 최댓값이란, i = a라고할 때, 0부터 a까지의 수열 중에서 a가 선택되었을 때의 가장 큰 연속합을 의미한다. 아래의 수열을 예를 들어 설명해보겠다.

 

10 -4 3 1 5 6 -35 12 21 -1

 

i=0일 때 -> 10

i=1일 때 -> i=0일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 6

i=2일 때 -> i=1일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 9

i=3일 때 -> i=2일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 10

i=4일 때 -> i=3일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 15

i=5일 때 -> i=4일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 21

i=6일 때 -> i=5일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> -14

i=7일 때 -> i=6일때의 최댓값이 음수이므로 더하지 않는 것이 최대값 -> 12

i=8일 때 -> i=7일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 33

i=9일 때 -> i=8일때의 최댓값이 양수이므로 그 값을 더한 값이 최대값 -> 32

 

따라서 최댓값이 33임을 알 수 있다. 이 때의 시간 복잡도는 O(n)이다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int[] seq = new int[n];
        seq[0= Integer.parseInt(st.nextToken());
        int maxCandidate = seq[0];
        int max = maxCandidate;
        
        for(int i = 1; i < n; i++) {
            seq[i] = Integer.parseInt(st.nextToken());
            
            if(maxCandidate > 0) {
                maxCandidate = seq[i] + maxCandidate;
            } else {
                maxCandidate = seq[i];
            }
            
            if(max < maxCandidate) {
                max = maxCandidate;
            }
        }
        
        System.out.println(max);
    }
}
 
 

 

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/11662

 

11662번: 민호와 강호

민호와 강호가 2차원 좌표 평면 위에 있다. 민호는 점 A(Ax, Ay)에서 점 B(Bx, By)를 향해 걸어가고 있고, 강호는 점 C(Cx, Cy)에서 점 D(Dx, Dy)를 향해 걸어가고 있다. 민호와 강호는 동시에 출발하고, 민호가 점 B에 도착하는 순간 강호도 점 D에 도착한다. 또, 두 사람은 항상 일정한 속도로 걸어간다. 두 사람의 거리가 가장 가까울 때, 거리를 구하는 프로그램을 작성하시오. 두 점 (x1, y1), (x2, y2)사이의 거리

www.acmicpc.net

민호가 A -> B로, 강호가 C -> D로 걸어가는데 둘이 도착하는 시간이 같다. 이 때 민호와 강호가 가장 가까울 때의 거리를 구하는 문제이다. 별로 어려운 문제는 아니다. 민호가 가는 거리와 강호가 가는 거리를 일정 간격(interval)로 쪼개준 다음에 조금씩 이동시키면서 매번 거리를 구한 후 그 중에 가장 작은 값을 구하면 된다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        double aX1 = Double.parseDouble(st.nextToken());
        double aY1 = Double.parseDouble(st.nextToken());
        double aX2 = Double.parseDouble(st.nextToken());
        double aY2 = Double.parseDouble(st.nextToken());
        double cX1 = Double.parseDouble(st.nextToken());
        double cY1 = Double.parseDouble(st.nextToken());
        double cX2 = Double.parseDouble(st.nextToken());
        double cY2 = Double.parseDouble(st.nextToken());
        
        int interval = 1000000;
        
        double aDX = (aX2 - aX1) / interval;
        double aDY = (aY2 - aY1) / interval;
        double cDX = (cX2 - cX1) / interval;
        double cDY = (cY2 - cY1) / interval;
        
        double min = getDistance(aX1, aY1, cX1, cY1);
        
        for(int i = 1; i <= interval; i++) {
            double tmp = getDistance(aX1 + aDX * i, aY1 + aDY * i, cX1 + cDX * i, cY1 + cDY * i);
            
            if(tmp < min) {
                min = tmp;
            }
        }
        
        System.out.println(min);
    }
    
    private static double getDistance(double x1, double y1, double x2, double y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2+ Math.pow(y2 - y1, 2));
    }
}
 
 

 

문제를 맞추고 난 후, 여느때와 다름없이 상위권 풀이와 비교해보니 어째 시간이 좀 길게 나오기도 하고, 푸는 방법도 달랐다. 구글링을 좀 해보니 '삼분탐색'이라고 하더라. 삼분탐색에 대한 자세한 설명은 아래 링크를 참고하면 좋다.

https://m.blog.naver.com/PostView.nhn?blogId=kks227&logNo=221432986308&proxyReferer=https%3A%2F%2Fwww.google.com%2F

 

삼분 탐색(Ternary Search)

안녕하세요. 이번에 들고 온 주제는 parametric search의 방법 중 하나인 삼분 탐색(ternary search)입니다...

blog.naver.com

근데 삼분탐색은, 최솟값을 구하는 경우 그 함수(여기선 i를 변수로 하는 함수라고 생각하면 된다)가 아래로 볼록할 때만 사용할 수 있다고 하더라. 근데 이게 아래로 볼록한 함수인지 어떻게 확신할 수 있지? 나는 그냥 수학적으로 직접 증명해보기로 했다. 고등학교 때 배웠던 미분이 약간 필요하다. 수학이라면 진절머리가 나는 사람이라면 그냥 스킵해도 좋다.

 

깔끔한 글씨로 계산해보려 했으나 실패...

일단 수학 시간이 아니므로 간단하게만 설명하겠다. f'(i) 의 분모는 항상 양수일 것이다.

분자는 (B^2 + D^2) * i + AB + CD인데, i의 계수는 항상 양수인 일차함수이므로 이 함수는 f'(i) 값이 음수에서 양수로 바뀌는, 즉 감소함수에서 증가함수로 바뀌는 함수라는 것을 알 수 있다. 아래로 볼록이라는 뜻이다. '이 문제에서 i는 0보다 크거나 개발자가 정한 최대값(interval)보다 작은 정수인데, 극소값이 존재하는 범위가 이 범위 밖에 벗어나면 그냥 증가함수나 감소함수가 되는 거 아닌가?!'라고 할 수도 있으나 삼분탐색은 그런 경우에도 사용 가능하다. 이분탐색보다 넓은 범위에서 사용 가능하다고 생각하면 될 것 같다. B^2 + D^2 = 0인 경우도 증가 혹은 감소함수가 될테니 설명 가능하다. (물론 AB+CD=0일 경우, 상수함수가 될텐데 이 경우에도 삼분탐색이 가능하긴 하다. 자세한 설명은 생략) 

 

이렇게 아래로 볼록이라는 걸 확실히 알 수 있을 때 삼분탐색을 이용해야하는 거 아닐까? 근데 생각해보자. 저렇게까지 계산했는데 굳이 삼분탐색을 이용해야할까?; 이왕 미분한거 극값이 나오는 i를 구해버리면 되지 않는가?!? 그래서 위의 계산식에 보면, f'(i) = 0일 때의 i값을 계산해놓았다. 분모가 0일때는 NaN이고, i가 범위를 벗어나는 경우도 있을 것이다. 그 경우에는 최소 값이 그래프의 양 끝 값 중 하나일 수밖에 없다. 

 

삼분탐색으로 푼 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        double aX = Double.parseDouble(st.nextToken());
        double aY = Double.parseDouble(st.nextToken());
        double bX = Double.parseDouble(st.nextToken());
        double bY = Double.parseDouble(st.nextToken());
        double cX = Double.parseDouble(st.nextToken());
        double cY = Double.parseDouble(st.nextToken());
        double dX = Double.parseDouble(st.nextToken());
        double dY = Double.parseDouble(st.nextToken());
        
        int interval = 1000000;
        
        double aDX = (bX - aX) / interval;
        double aDY = (bY - aY) / interval;
        double cDX = (dX - cX) / interval;
        double cDY = (dY - cY) / interval;
        
        int lo = 0;
        int hi = interval;
        
        while(hi - lo >= 3) {
            int p = (2 * lo + hi) / 3;
            int q = (lo + 2 * hi) / 3;
            
            double pVal = getDistance(aX + aDX * p, aY + aDY * p, cX + cDX * p, cY + cDY * p);
            double qVal = getDistance(aX + aDX * q, aY + aDY * q, cX + cDX * q, cY + cDY * q);
            
            if(pVal < qVal) {
                hi = q - 1;
            } else {
                lo = p + 1;
            }
        }
        
        double min = getDistance(aX + aDX * hi, aY + aDY * hi, cX + cDX * hi, cY + cDY * hi);
        
        for(int i = lo; i < hi; i++) {
            double temp = getDistance(aX + aDX * i, aY + aDY * i, cX + cDX * i, cY + cDY * i);
            
            if(temp < min) {
                min = temp;
            }
        }
        
        System.out.println(min);
    }
 
    private static double getDistance(double x1, double y1, double x2, double y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2+ Math.pow(y2 - y1, 2));
    }
}
 
 

 

미분을 통한 극솟값 계산을 통해 푼 풀이는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        double aX1 = Double.parseDouble(st.nextToken());
        double aY1 = Double.parseDouble(st.nextToken());
        double aX2 = Double.parseDouble(st.nextToken());
        double aY2 = Double.parseDouble(st.nextToken());
        double cX1 = Double.parseDouble(st.nextToken());
        double cY1 = Double.parseDouble(st.nextToken());
        double cX2 = Double.parseDouble(st.nextToken());
        double cY2 = Double.parseDouble(st.nextToken());
        
        int interval = 1000000;
        
        double aDX = (aX2 - aX1) / interval;
        double aDY = (aY2 - aY1) / interval;
        double cDX = (cX2 - cX1) / interval;
        double cDY = (cY2 - cY1) / interval;
        
        double i = -1 * ((aX1 - cX1) * (aDX - cDX) + (aY1 - cY1) * (aDY - cDY)) / (Math.pow(aDX - cDX, 2+ Math.pow(aDY - cDY, 2));
        
        if(!Double.isNaN(i) && i < interval && i > 0) {
            double min = getDistance(aX1 + aDX * i, aY1 + aDY * i, cX1 + cDX * i, cY1 + cDY * i);
            System.out.println(min);
        } else {
            double min1 = getDistance(aX1, aY1, cX1, cY1);
            double min2 = getDistance(aX2, aY2, cX2, cY2);
            
            System.out.println(Math.min(min1, min2));
        }
    }
    
    private static double getDistance(double x1, double y1, double x2, double y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2+ Math.pow(y2 - y1, 2));
    }
}
 
 

개인적으로 필자는 1번 풀이 혹은 3번 풀이가 좋은 풀이라고 본다. 즉, 이 문제를 굳이 삼분탐색으로 풀 이유가 없는 것 같다. 이유는, 아래로 볼록인 게 확실하지 않은 상황에서는 삼분탐색을 해선 안된다. 아래로 볼록인 걸 증명하기 위해 수학적 계산을 하면, 이 문제의 경우 미분 가능하기 때문에 그냥 극값을 구해버리는 게 낫다. 시간복잡도 측면에서 압도적으로 유리하기 때문이다. 필자가 남겨놓은 삼분탐색 참고를 위한 링크 글에서도 나오지만, 삼분탐색은 함수가 미분 불가능할 때만 써야할 것 같다.

 

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/10816

 

10816번: 숫자 카드 2

첫째 줄에 상근이가 가지고 있는 숫자 카드의 개수 N(1 ≤ N ≤ 500,000)이가 주어진다. 둘째 줄에는 숫자 카드에 적혀있는 정수가 주어진다. 숫자 카드에 적혀있는 수는 -10,000,000보다 크거나 같고, 10,000,000보다 작거나 같다. 셋째 줄에는 M(1 ≤ M ≤ 500,000)이 주어진다. 넷째 줄에는 상근이가 몇 개 가지고 있는 숫자 카드인지 구해야 할 M개의 정수가 주어지며, 이 수는 공백으로 구분되어져 있다. 이수도 -10,00

www.acmicpc.net

문제를 보자마자 전형적인 이분탐색문제라고 생각했다. 그렇게 생각하고 문제를 풀었으나, 시간초과...-_- 다른 언어에서도 그런지는 모르겠으나 적어도 자바에서는 이분탐색으로 이 문제를 풀면 시간초과가 뜬다. 이분탐색으로 푼 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] cards = new int[n];
        
        for(int i = 0; i < n; i++) {
            cards[i] = Integer.parseInt(st.nextToken());
        }
        
        Arrays.sort(cards);
        
        int m = Integer.parseInt(br.readLine());
        
        StringBuffer sb = new StringBuffer();
        st = new StringTokenizer(br.readLine());
        
        for(int i = 0; i < m; i++) {
            int x = Integer.parseInt(st.nextToken());
            int cnt = getCnt(cards, x, n);
            sb.append(cnt + " ");
        }
        
        System.out.println(sb);
    }
 
    private static int getCnt(int[] cards, int x, int n) {
        int left = 0;
        int right = n - 1;
        int cnt = 0;
        
        while(left <= right) {
            int mid = (left + right) / 2;
            
            if(x < cards[mid]) {
                right = mid - 1;
            } else if(x > cards[mid]) {
                left = mid + 1;
            } else {
                cnt = 1;
                int tmp = mid;
                
                while(--tmp >= 0 && cards[tmp] == x) {
                    cnt++;
                }
                
                while(++mid < n && cards[mid] == x) {
                    cnt++;
                }
                
                break;
            }
        }
        
        return cnt;
    }
}
 
 

 

그럼 대체 어떻게 풀어야한단 말인가... 고뇌를 하던 와중, 숫자의 범위가 -10,000,000 ~ 10,000,000이라는 게 눈에 들어왔다. 그럼 20,000,001개의 수이고 int 는 4 byte니까 20,000,001 X 4 = 80,000,004 -> 약 80MB~!!!! 생각보다 배열이 크지 않구만~! 배열에 모든 수의 개수를 저장하면 되겠다는 생각이 들었다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] cnt = new int[20_000_001];
        
        for(int i = 0; i < n; i++) {
            cnt[Integer.parseInt(st.nextToken()) + 10_000_000]++;
        }
        
        int m = Integer.parseInt(br.readLine());
        st = new StringTokenizer(br.readLine());
        StringBuffer sb = new StringBuffer();
        
        for(int i = 0; i < m; i++) {
            sb.append(cnt[Integer.parseInt(st.nextToken()) + 10_000_000] + " ");
        }
        
        System.out.println(sb);
    }
}
 
 

 

문제를 만든 사람이 어떤 의도로 만든 문제인지는 모르겠으나, 현재 이 문제는 메모이제이션으로 푸는게 가장 효과적이다. 근데 수의 범위가 더 커진다면, 메모리를 너무 많이 사용해야함으로 이분 탐색이 더 나은 알고리즘이 될지도 모르겠다.

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/2004

 

2004번: 조합 0의 개수

첫째 줄에 정수 n, m(0≤m≤n≤2,000,000,000, n!=0)이 들어온다.

www.acmicpc.net

이 문제를 풀기 위해서는, 일단 고등학교 때 수학 시간에 배웠던 조합에 대한 기억을 되살려야한다. 많은 걸 기억해낼 필요는 없고, 밑에 있는 공식 하나만 기억하면 된다.

조합의 값 끝자리 0의 개수를 구하기 위해서는, 조합의 값을 소인수분해 했을 때 10 (2 X 5)을 얼마나 많이 가지고 있는지를 구하면 된다. 예를 들면, 50은 5 X 10이니까 0이 한 개이다. 342000은 342 X 10 X 10 X 10이니까 0이 세 개이다.

 

당연히 조합값을 다 구한 다음에 계산하는 것은 불가능에 가깝다. n, m의 최대값이 int의 최대값에 가까운 상황에서 팩토리얼 계산이 들어가버리면... 아무리 큰 자료형을 쓴다고 해도 그것들을 다 담아내는 것은 거의 불가능할 것이다. 어차피 우리가 궁금한 것은 조합값 자체가 아니라 조합값을 소인수분해했을 때 포함되어있는 2와 5의 개수이다.

 

팩토리얼 계산에 들어가는 값들을 일일히 소인수분해하여 2와 5의 개수를 셀 수도 있을 것이다. 필자는 처음에 그렇게 생각하고 풀었으나... 시간초과가 떴다...ㅠㅠ 시간초과로 실패한 필자의 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 
public class Main {
    public static void main(String[] args) throws Exception {
        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());
        
        long twoCnt = 0;
        long fiveCnt = 0;
        
        twoCnt = getCnt(n, 2- getCnt(m, 2- getCnt(n - m, 2);
        fiveCnt = getCnt(n, 5- getCnt(m, 5- getCnt(n - m, 5);
        
        System.out.println(Math.min(twoCnt, fiveCnt));
    }
 
    private static long getCnt(int n, int a) {
        long cnt = 0;
        
        for(int i = a; i <= n; i += a) {
            int temp = i;
            
            while(temp != 0 && temp % a == 0) {
                cnt++;
                temp /= a;
            }
        }
        
        return cnt;
    }
}
 

 

시간초과가 뜨자 위의 코드를 여러 방식으로 바꾸어 보았다. 한 번의 연산에서 2와 5를 모두 계산하고, 조합 자체의 수학 공식을 변형하여 더 적은 연산횟수로 바꿔보기도 했으나, 일일히 소인수분해를 해서 개수를 확인한다는 컨셉 내에서는 결국 시간초과를 벗어날 수 없었다. 결국 구글링 ㄱㄱ...ㅠㅠ

 

이제 구글링을 통해 풀어낸 풀이에 대한 설명을 하겠다. 소인수분해를 통해 2와 5의 개수를 구한다는 기본컨셉은 동일하다. 근데 그 개수를 구하는 방식에 차이가 있다.

 

59!을 소인수 분해했을 때 5가 몇 개 있는지를 구하는 과정을 예를 들어 설명해보겠다.

59! = 59 X 58 X 57 X 56 X ..... X 2 X 1

즉, 1 ~ 59 수의 곱이다. 그렇다면 이 중 5의 배수는 몇 개일까? 59 / 5 = 11개이다. 11개 이 외의 수들은 체크해볼 필요도 없이 5를 소인수로 포함하고 있지 않을 것이다. 11개의 수를 나열해보면, 55 X 50 X 45 ... 10 X 5 이다. 59!은 최소한 11개의 5를 소인수로 가지고 있는 것이다. 11개의 5는 이제 카운트 되었으니, 각각의 11개의 수들을 5로 나눠보면,

11 X 10 X 9 X ... X 2 X 1 이 된다. 여기에서 5의 배수의 개수는 11 /5 = 2개가 된다. 이런 방식을 통해, 59!을 소인수분해했을 때 5는 총 13개가 있다는 것을 알 수 있다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
public class Main {
    public static void main(String[] args) throws Exception {
        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 twoCnt = get(n, 2- get(m, 2- get(n - m, 2);
        int fiveCnt = get(n, 5- get(m, 5- get(n - m, 5);
        
        System.out.println(Math.min(twoCnt, fiveCnt));
    }
    
    private static int get(int n, int k) {
        int cnt = 0;
        
        while(n >= k) {
            cnt += n / k;
            n /= k;
        }
        
        return cnt;
    }
}
 
 

 

위와 같은 방식으로 소인수의 개수를 구할 수 있다는 아이디어를 떠올리는 것이 어렵다... 유레카!하고 떠오르면 좋겠다만 천재가 아닌 이상 그게 쉽진 않을 것 같다. 결국 이런 풀이의 경우 외우고 있는 것이 좋을 것 같다는 생각이 든다.

 

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/1697

 

1697번: 숨바꼭질

문제 수빈이는 동생과 숨바꼭질을 하고 있다. 수빈이는 현재 점 N(0 ≤ N ≤ 100,000)에 있고, 동생은 점 K(0 ≤ K ≤ 100,000)에 있다. 수빈이는 걷거나 순간이동을 할 수 있다. 만약, 수빈이의 위치가 X일 때 걷는다면 1초 후에 X-1 또는 X+1로 이동하게 된다. 순간이동을 하는 경우에는 1초 후에 2*X의 위치로 이동하게 된다. 수빈이와 동생의 위치가 주어졌을 때, 수빈이가 동생을 찾을 수 있는 가장 빠른 시간이 몇 초 후인지

www.acmicpc.net

전형적인 BFS문제이다. 주의할 점은,

1. 좌표값이 0 일 때는 X-1, X*2를 해선 안된다.

2. 좌표값이 K(동생의 위치) 보다 클 때는 X+1, X*2를 할 필요가 없다.

 

코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());
        
        if(n >= k) {
            System.out.println(n - k);
        } else {
            boolean[] visited = new boolean[k * 2 + 1];
            visited[n] = true;
            Queue<Status> queue = new LinkedList<Status>();
            queue.offer(new Status(n, 0));
            
            int answer = 0;
            
            outer : while(!queue.isEmpty()) {
                Status poll = queue.poll();
                int position = poll.getPosition();
                int time = poll.getTime();
                    
                for(int i = 0; i < 3; i++) {
                    int nextPosition = getNextPosition(position, k, i);
                    
                    if(nextPosition != -1 && !visited[nextPosition]) {
                        
                        if(nextPosition == k) {
                            answer = time + 1;
                            break outer;
                        }
                        
                        visited[nextPosition] = true;
                        queue.offer(new Status(nextPosition, time + 1));
                    }
                }
            }
            
            System.out.println(answer);
        }
    }
    
    private static int getNextPosition(int position, int k, int idx) {
        int nextPosition = -1;
        
        switch (idx) {
        case 0:
            if(position > 0) {
                nextPosition = position - 1;
            }
            break;
        case 1:
            if(position > 0 && position < k) {
                nextPosition = position * 2;
            }
            break;
        default:
            if(position < k) {
                nextPosition = position + 1;
            }
            break;
        }
        
        return nextPosition;
    }
    
    private static class Status {
        private int position;
        private int time;
        
        public Status(int position, int time) {
            this.position = position;
            this.time = time;
        }
        
        public int getPosition() {
            return position;
        }
        public int getTime() {
            return time;
        }
    }
}
 
 

 

 

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/2225

 

2225번: 합분해

첫째 줄에 답을 1,000,000,000으로 나눈 나머지를 출력한다.

www.acmicpc.net

높은 정답률(현재 42%)에 비해 개인적으로 푸는데 고생을 좀 했던 문제이다. 총 2가지 풀이법을 소개할 것인데, 1. 내가 이 문제를 처음 푼 방법, 2. 인터넷에서 검색할 수 있는 일반적인(?) 방법이다.

 

N = 5, K = 3일 때를 직접 일일히 세어가면서 구한다고 생각해보자. 일단 2개로 먼저 쪼개보면, (5, 0), (4, 1), (3, 2), (2, 3), (1, 4), (0, 5)가 될 것이다. 여기서 한 번 더 나눠야 하는데, 이 때는 마지막 수를 다시 2개로 쪼갠다고 생각하면 된다. 그러면 (5, 0, 0), (4, 1, 0), (4, 0, 1), (3, 2, 0), (3, 1, 1), (3, 0, 2)... 이 식으로 될텐데 아래 그림을 보면 이해가 될 것이다.

여기서 내가 고려한 점은

1. K를 위와 같은 방식으로 계속 증가시키면서, 마지막 수의 개수를 카운트한다. (마지막 수만 경우의 수에 영향을 미치므로)

2. 마지막 수는 0 ~ N까지의 수인데, K가 증가할 때 임의의 i의 개수는 바로 직전 마지막 수들 중 i보다 큰 수들의 개수를 모두 더한 값이 된다. (위의 그림을 예로 들면, 3의 개수는 바로 직전의 3 ~ 5의 개수를 합친 것과 같다)

 

이를 고려하여 풀이한 소스는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());
        
        int[] cnt = new int[n + 1];
        cnt[n] = 1;
        
        for(int i = 2; i <= k; i++) {
            int[] tmp = new int[n + 1];
            tmp[n] = 1;
            
            for(int j = n - 1; j >= 0; j--) {
                tmp[j] = tmp[j + 1+ cnt[j];
                tmp[j] %= 1_000_000_000;
            }
            
            cnt = tmp;
        }
        
        long sum = 0;
        
        for(int i = 0; i <= n; i++) {
            sum += cnt[i];
            sum %= 1_000_000_000;
        }
        
        System.out.println(sum);
    }
}
 
 

 

이번에는 인터넷에서 흔히 볼 수 있는 일반적인(?) 풀이법에 대한 설명이다.

 

N = 5, K = 3일 때를 구한다고 가정해보면, 

0 ~ 5의 수 중 3개를 선택하여 5를 만드는 경우의 수 = 첫번째 수를 0으로 선택하고, 0 ~ 5의 수 중 2개를 선택하여 5를 만드는 경우의 수 + 첫번째 수를 1으로 선택하고, 0 ~ 4의 수 중 2개를 선택하여 4를 만드는 경우의 수 .....

이런 식으로 될텐데, 수식화 해보면 아래와 같다.

 

(N=5, K=3) = (N=5, K=2) + (N=4, K=2) + (N=3, K=2) + (N=2, K=2) + (N=1, K=2) + (N=0, K=2)

 

여기서 우리는 규칙성을 확인할 수 있는데, 이를 다시 수식화 해보면 아래와 같다.

 

(N=i, K=j) = (N=i, K=j-1) + (N=i-1, K=j-1) + (N=i-2, K=j-1) + ..... + (N=0, K=j-1)

<-> (N=i, K=j) = (N=i, K=j-1) + (N=i-1, K=j)

 

위에서 얻어낸 점화식과 함께 아래사항을 초기값으로 이용하면 답을 도출해낼 수 있다.

1. K = 1 일 때 경우의 수는 1이다.

2. N = 1 일 때 경우의 수는 K이다.

 

코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());
        
        int[][] dp = new int[n + 1][k + 1];
        
        for(int i = 1; i <= k; i++) {
            dp[1][i] = i;
        }
        
        for(int i = 1; i <= n; i++) {
            dp[i][1= 1;
        }
        
        for(int i = 2; i <= n; i++) {
            for(int j = 2; j <= k; j++) {
                dp[i][j] = (dp[i][j - 1+ dp[i - 1][j]) % 1_000_000_000;
            }
        }
        
        System.out.println(dp[n][k]);
    }
}
 

 

두 코드 모두 동일한 시간복잡도를 가지고 있지만, 개인적으로 두 번째 풀이방법이 더 기억하기도, 이해하기도 쉬운 것 같다. 

 

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/1654

 

1654번: 랜선 자르기

첫째 줄에는 오영식이 이미 가지고 있는 랜선의 개수 K, 그리고 필요한 랜선의 개수 N이 입력된다. K는 1이상 10,000이하의 정수이고, N은 1이상 1,000,000이하의 정수이다. 그리고 항상 K ≦ N 이다. 그 후 K줄에 걸쳐 이미 가지고 있는 각 랜선의 길이가 센티미터 단위의 정수로 입력된다. 랜선의 길이는 231-1보다 작거나 같은 자연수이다.

www.acmicpc.net

 

결론부터 말하자면, 이 문제는 이분탐색 문제이다. 이분탐색으로 풀면된다는 사실을 알고 이 문제를 보면, 사실 그다지 어려운 문제는 아니다. 근데 이분탐색으로 풀면 되겠다고 생각해내는 것, 그게 어렵다. 어쩌면 그게 알고리즘의 전부일지도 모르겠다...ㅠㅠ

 

그리고 이 문제의 정답비율이 유독 낮은(현재 19%) 이유를 필자가 예상하건데, 함정이 있다. 문제에서는 랜선의 길이가 2^31 -1(int의 최댓값)보다 작거나 같은 자연수라고 나오는데, 이는 랜선의 길이를 int로 해도 되겠다라고 생각하게 만들지만 그렇지 않다. 이분 탐색을 위해 두 길이를 더하는 순간 넘어가버리게 된다... 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int k = Integer.parseInt(st.nextToken());
        int n = Integer.parseInt(st.nextToken());
        int[] wires = new int[k];
        long sum = 0;
        
        for(int i = 0; i < k; i++) {
            wires[i] = Integer.parseInt(br.readLine());
            sum += wires[i];
        }
        
        long answer = 1;
        long left = 1;
        long right = sum / n;
        
        while(left <= right) {
            long mid = (left + right) / 2;
            
            int cnt = checkPossibleLineNo(wires, mid, k);
            
            if(cnt >= n) {
                left = mid + 1;
                
                if(mid > answer) {
                    answer = mid;
                }
            } else {
                right = mid - 1;
            }
        }
        
        System.out.println(answer);
    }
 
    private static int checkPossibleLineNo(int[] wires, long mid, int k) {
        int cnt = 0;
        
        for(int i = 0; i < k; i++) {
            cnt += wires[i] / mid;
        }
        
        return cnt;
    }
}
 
 

 

반응형
반응형

문제링크 : https://www.acmicpc.net/problem/1931

 

1931번: 회의실배정

(1,4), (5,7), (8,11), (12,14) 를 이용할 수 있다.

www.acmicpc.net

 

어떻게 하면 가장 적은 횟수의 비교를 통해 최댓값을 얻어낼 수 있을지를 생각해내는 게 중요한 문제이다. 필자는 처음에 정말 단순하게 '길이(Duration)가 짧은 회의부터 우선적으로 넣으면 되지 않을까?'라고 생각을 했으나, 이 경우에는 회의를 넣어도 되는지 판단하는 시점에서 이미 추가된 "모든" 회의들과 겹치는 부분이 없는지를 비교해야했다. 이 경우의 시간복잡도는 O(N^2)이다. 그러다 고민 끝에 길이따위는 중요하지 않고, 끝나는 시간만 생각하면 되겠구나!라는 걸 깨달았다. 긴~ 막대기가 있다고 생각해보자. 그 막대기에 최대한 많은 조각을 넣어야한다고 생각해보면, 왼쪽부터 작은거 순서대로 차근차근 넣어주면 된다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        Meeting[] meetings = new Meeting[n];
        
        for(int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            meetings[i] = new Meeting(start, end);
        }
        
        Arrays.sort(meetings);
        
        int cnt = 1;
        Meeting pastMeeting = meetings[0];
        
        for(int i = 1; i < n; i++) {
            Meeting meeting = meetings[i];
            
            if(!meeting.intersect(pastMeeting)) {
                cnt++;
                pastMeeting = meetings[i];
            }
        }
        
        System.out.println(cnt);
    }
    
    private static class Meeting implements Comparable<Meeting> {
        private int start;
        private int end;
        
        public Meeting(int start, int end) {
            this.start = start;
            this.end = end;
        }
 
        public int getStart() {
            return start;
        }
 
        public int getEnd() {
            return end;
        }
 
        @Override
        public int compareTo(Meeting obj) {
            return this.end - obj.getEnd();
        }
        
        public boolean intersect(Meeting obj) {
            return obj.getStart() < this.end && obj.getEnd() > this.start;
        }
    }
}
 
 

 

상위권 풀이들과 비교해보니, 어째 시간이 좀 더 길게 나왔다. 차이를 분석해보니, 굳이 겹치는(intersect) 여부를 보지 않고, 바로 이전의 end값과 비교시점의 start만 비교하면 되더라. 이 경우에는 오버라이드하는 함수 "compareTo" 에 조건이 일부 추가된다. 그 이유는 길이가 0인 회의들 때문이다. 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
 
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        Meeting[] meetings = new Meeting[n];
        
        for(int i = 0; i < n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            meetings[i] = new Meeting(start, end);
        }
        
        Arrays.sort(meetings);
        
        int cnt = 1;
        int cursor = meetings[0].getEnd();
        
        for(int i = 1; i < n; i++) {
            Meeting meeting = meetings[i];
            
            if(meeting.getStart() >= cursor) {
                cnt++;
                cursor = meeting.getEnd();
            }
        }
        
        System.out.println(cnt);
    }
    
    private static class Meeting implements Comparable<Meeting> {
        private int start;
        private int end;
        
        public Meeting(int start, int end) {
            this.start = start;
            this.end = end;
        }
 
        public int getStart() {
            return start;
        }
 
        public int getEnd() {
            return end;
        }
 
        @Override
        public int compareTo(Meeting obj) {
            int ret = this.end - obj.getEnd();
            
            if(ret == 0) {
                ret = this.start - obj.getStart();
            }
            
            return ret;
        }
    }
}
 
s

 

반응형

+ Recent posts