[Java / 백준 10986] 나머지 합

isohyeon·2022년 8월 12일
9
post-custom-banner

📢 이번 문제는 합배열과 누적합에 대한 사전지식이 필요합니다! 합배열과 누적합에 대한 설명이 필요하다면 아래의 글을 참고해주세요 😉
[Java/백준 11659] 구간 합 구하기4

문제

수 N개 A1, A2, ..., AN이 주어진다. 이때, 연속된 부분 구간의 합이 M으로 나누어 떨어지는 구간의 개수를 구하는 프로그램을 작성하시오.
즉, Ai + ... + Aj (i ≤ j) 의 합이 M으로 나누어 떨어지는 (i, j) 쌍의 개수를 구해야 한다.
백준 10986 상세보기
❗ 시간 제한이 1초임을 고려해야 한다.

분석

구간 합(S[j]-S[i-1])을 M으로 나눈 나머지가 0인 (i, j)의 값을 구해야한다.
수학적으로 정리해보면 다음과 같다.

  1. (S[j](S[j] - S[i1])S[i-1]) % MM =0= 0
  2. (S[j](S[j] % M)M) - (S[i1](S[i-1] % M)M) =0= 0
  3. S[j]S[j] % M=M= S[i1]S[i-1] % MM

위의 식을 토대로 구현해야할 코드를 단계적으로 생각해보자.

  1. 합배열 s를 생성할 때, %M 처리한 결과를 저장한다.
    합배열 생성 과정
  2. S[i] % M == 0 인 수를 세어 결과 값에 더한다.
    S[i] % M 값이 0이라는 뜻은 원본 배열 A의 (1~i)의 구간 합이 이미 M으로 나누어 떨어진다는 뜻이기 때문이다.
    합배열 % M 그림
  3. S[j] % M == S[i-1] % M 을 만족하는 (i,j)의 수를 구한다. 즉, 나머지 값이 같은 인덱스 중 2개를 뽑는 모든 경우의 수를 구하면 된다.
    • 나머지 값이 같은 인덱스의 수를 저장하기 위한 배열 cnt를 생성한다.
      이때, M으로 나눈 나머지가 될 수 있는 수는 0부터 M-1까지 가능하다. 따라서 크기가 M인 배열을 생성한다.
      cnt 배열 그림
    • cnt 배열을 통해 나머지 값이 같은 인덱스 중 2개를 뽑는 모든 경우의 수를 구한다. 위의 예에서는 0이 3개, 1이 2개이므로 3C2_3C_22C2_2C_2의 합을 구하고 결과 값에 더한다.
      nCr_nC_r : n개 중에서 r개를 뽑는 모든 경우의 수
      조합 계산

소스 코드

import java.util.*;
import java.io.*;

public class Main {
    public static void main(String args[]) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        // 1. N(수의 개수), M(나누기 할 수) 입력받기
        int N = Integer.parseInt(st.nextToken());   // 수의 개수
        int M = Integer.parseInt(st.nextToken());   // 나누기 할 수
        long result = 0;                            // M으로 나누어떨어지는 (i,j) 쌍의 개수
        long[] S = new long[N + 1];                 // 합배열
        long[] cnt = new long[M];                   // 같은 나머지의 인덱스를 카운트하는 배열

        // 2. N개의 수 입력받으면서 누적합을 M으로 나눈 나머지를 배열 S에 저장한다.
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i < N + 1; i++) {
            S[i] = (S[i - 1] + Integer.parseInt(st.nextToken())) % M;
            // 0~i까지의 합을 M으로 나눈 나머지가 0인 경우의 수 카운팅
            if(S[i] == 0) {
                result++;
            }
            // 나머지가 같은 인덱스의 수 카운팅
            cnt[(int) S[i]]++;
        }

        // 3. S[j] % M == S[i-1] % M 을 만족하는 (i,j)의 수를 결과값에 더한다.
        // 즉, cnt[i](i가 나머지인 인덱스의 수)에서 2가지를 뽑는 경우의 수 카운팅한다.
        for(int i=0; i<M; i++) {
            if(cnt[i] > 1) {
                result += (cnt[i]* (cnt[i]-1) / 2);
            }
        }
        System.out.println(result);
    }
}

풀이 정리

Tip 1. 시간 제한을 고려하자 !

사실 이번 문제는 시간 제한이 없었더라면 간단하게 풀 수 있었을지도 모른다. 첫번째 시도에는 중첩 반복문을 통해 모든 구간 합 중에서 S[j] - S[i-1]) % M == 0 조건을 만족하는 수를 구하는 코드를 작성했지만 시간 초과로 실패했다😂 아무래도 모든 구간 합을 검사하는 부분에서 시간이 오래 걸린것 같다.
아래의 코드는 첫번째로 작성했던 코드이다.

import java.util.*;
import java.io.*;
public class Main {
    public static void main(String args[]) throws IOException {
        // 1. N(수의 개수), M(나누기 할 수) 입력받기
        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());

        // 2. N개의 수 입력받으면서 누적합 배열 S 생성하기
        st = new StringTokenizer(br.readLine());
        int[] S = new int[N+1];
        for(int i=1; i<N+1; i++) {
            S[i] = S[i-1] + Integer.parseInt(st.nextToken());
        }

        // 3. 구간 합(S[j]-S[i-1])를 M으로 나누었을 때 나머지가 0이 되는 (i,j) 쌍의 개수 계산
        int cnt = 0;
        for(int i=1; i<N+1; i++) {
            for(int j=i; j<N+1; j++) {
                if((S[j] - S[i-1]) % M == 0) {
                    cnt++;
                }
            }
        }

        System.out.println(cnt);
    }
}

Tip 2. 수학 계산을 활용하자 !

1초의 시간 제한을 맞추기 위해서는 연산을 줄일 수 있는 방법이 필요하다.
이번 문제에서는 S[j]S[j] % M=M= S[i1]S[i-1] % MM 를 도출하는 것과 조합( nCr_nC_r )을 사용하는 방법이 있었다. 물론 단번에 생각해내기는 어렵겠지만..😅 이번 기회를 통해 이런 방법도 있다는 것을 기억해두면 좋을 것 같다!

Tip 3. 오버플로우가 발생하지는 않는지 확인하자 !

연산 과정을 줄인 코드로 다시 시도했지만 또 다시 틀렸다는 결과가 나왔다. 다른 코드들과 비교해보니 데이터 타입이 int가 아닌 long타입으로 선언되어 있는 것을 발견했다.
문제의 입력 조건을 고려하면 int 타입이 모든 테스트 데이터를 수용하지 못한 것이 원인이 된 것 같다.👀

  • 1N1061 ≤ N ≤ 10^6
  • 2M1032 ≤ M ≤ 10^3
  • 0Ai1090 ≤ Ai ≤ 10^9

정수형 데이터의 타입을 결정할 때는 반드시 사용하고자 하는 데이터의 최대 크기를 고려해야 한다. 해당 타입이 표현할 수 있는 범위를 벗어난 데이터를 저장하면, 오버플로우가 발생해 전혀 다른 값이 저장될 수 있다.

  • 오버플로우(overflow)
    : 해당 타입이 표현할 수 있는 최대 범위보다 큰 수를 저장할 때 발생하는 현상
  • 언더플로우(underflow)
    : 해당 타입이 표현할 수 있는 최소 범위보다 작은 수를 저장할 때 발생하는 현상

💡 Java의 정수형 데이터 타입 중에서 long 타입이 데이터의 표현 범위가 가장 넓다! 따라서 코딩 테스트에서는 처음부터 long형으로 선언하는 것이 좋을 것 같다!


Reference

[book] Do it! 알고리즘 코딩 테스트 - 자바 편
기본 타입(primitive type) - 코딩의 시작, TCP School

profile
junior developer (●'◡'●)
post-custom-banner

6개의 댓글

comment-user-thumbnail
2022년 8월 18일

와우~! 이렇게 자세하게 정성껏 설명해 주셔서 이해가 정말 잘되네요. 많은 도움이 되었습니다. 감사합니다. ^^

답글 달기
comment-user-thumbnail
2023년 1월 8일

수 많은 블로그를 보고도 이해가 되지 않았는데 작성자님이 정리해주신 글을 보고 마침내 이해하게 되었습니다. 깔끔하게 정리해주셔서 정말 감사합니다 :)

답글 달기
comment-user-thumbnail
2023년 5월 5일

덕분에 이해했습니다 자세한 설명 감사합니다ㅠㅠ

답글 달기
comment-user-thumbnail
2023년 5월 15일

유튜브봐도 이해가 안되었는데 이거보고 이해가 되네요... 정말 감사합니다...

답글 달기
comment-user-thumbnail
2023년 10월 6일

안녕하세요 작성자님. 제 코드도 비슷하게 짰는데요 ArrayIndexOutofBound에러가 나는데 어딘지 못 찾겠어서 봐주실 수 있을까요,,

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Example10986_2 {

public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer tokenizer = new StringTokenizer(br.readLine());

    int n = Integer.parseInt(tokenizer.nextToken());
    int m = Integer.parseInt(tokenizer.nextToken());
    int count = 0;

    long[] intArr = new long[n + 1];
    long[] sumArr = new long[n + 1];
    int[] rmdCount = new int[m];

    tokenizer = new StringTokenizer(br.readLine());
    for (int i = 1; i <= n; i++) {
        intArr[i] = Integer.parseInt(tokenizer.nextToken());
        sumArr[i] = sumArr[i - 1] + intArr[i];
    }
    int remainder;

    for (int i = 1; i <= n; i++) {
        remainder = (int)sumArr[i] % m;
        if(remainder == 0 )
            count++;

        rmdCount[remainder]++;
    }
    
    for (int i = 0; i < m; i++) {
        if (rmdCount[i] > 1) {
            count += rmdCount[i] * (rmdCount[i] - 1) / 2;
        }
    }

    System.out.println(count);

}

}

답글 달기
comment-user-thumbnail
2024년 3월 6일

고수..

답글 달기