본문 바로가기

Programming/Rust로 Euler 문제 풀이

019. Counting Sundays (두 날자 사이에서, 매달 초가 일요일이 몇 번인지 카운트)

반응형

 

 

 

​문제 (English)

You are given the following information, but you may prefer to do some research for yourself.

  • 1 Jan 1900 was a Monday.
  • Thirty days has September, April, June and November. All the rest have thirty-one, Saving February alone, Which has twenty-eight, rain or shine. And on leap years, twenty-nine.
  • A leap year occurs on any year evenly divisible by 4, but not on a century unless it is divisible by 400. How many Sundays fell on the first of the month during the twentieth century (1 Jan 1901 to 31 Dec 2000)?

 

Answer: 171

문제 분석


두 날자 사이에서 매달 초가 일요일이 되는 달이 몇 개인지 세는 문제이다.

 

이러한 계산을 위해서는,

  • 그레고리역(현재 우리가 쓰는 양력이다.)의 윤년 규칙을 알아야 한다.
  • 요일이란 것이 법7(modulation 7)을 기반으로 하는 거여서, 법 7을 기반으로 한 계산에 익숙해야 한다.

 

풀이 1 


1900.1.1일이 월요일이라고 했으니, 다음과 같이 풀이 방법을 생각해 보자.

- 1900.1.1일부터 시작해서 한 달씩 건너 띄면서 날자를 세어본다.

- 즉, 1900.1.1일 다음은 같은 해의 2.1일. 1월이 31일이므로 1+31일 지난 것

- 그다음은 3.1일은, 윤년이 아니라면 2월달 28일 더해서 60일, 만약 윤년이라면 29일을 더한다. (1900년은 윤년이 아니기에 28을 더하면 되겠다.)

- 이렇게 더한 값을 %7 해준다. 60%7=4 --> 1:월, 2:화, 3:수, 4:목, 5:금, 6:토, 0:일 

- 이렇게 더해 나가는데, 만약 주어진 기간이 1902년 2월 1일부터라면, 그 이전인 1902.1.1일까지는 날수만을 세고 그날의 요일은 계산하지 않는다.

- 주어진 기간 이후부터는(예를 들어 1902.2.1일부터), 매달 초일이 일요일인지를 계산해 보고, 카운트를 한다.

 

여기서 해당 연도가 윤년인지는 다음과 같은 규칙. 윤년에는 2월이 29일이다. 

  • year가 4의 배수이면 윤년 
  • 그러나, year가 100의 배수이면 윤년이 아니다.
  • 또 그러나, year가 400의 배수이면 윤년이다.

이를 함수로 구현하면 다음과 같다.

fn is_leap(y:usize) -> bool {
    if y % 400 == 0 {return true;}
    if y % 100 == 0 {return false;}
    if y % 4 == 0 {return true;}
    return false;
}

 

윤년인지는, 간단하게 한 줄로 표현할 수도 있다.

if (y%400==0 || (y%4==0 && y%100!=0))

 

아래는 위와 같은 논리로 두 Date 사이에서, 매달 1월 1일이 일요일인 개수를 세는 함수이다.

#[cfg(test)]
fn answer((y1,m1,d1):(usize,usize,usize), (y2,m2,_d2):(usize,usize,usize)) -> usize{
    let month = [0,31,28,31,30,31,30,31,31,30,31,30,31];
    
    let mut ans:usize = 0;
    let mut diff:usize = 1;
    'outer: for y in 1900..=y2{
        for m in 1..=12 {
            if y == y2 && m >= m2 { break 'outer;}
            diff += if is_leap(y) && m==2 {month[m]+1} else {month[m]};
            
            if y < y1 || (y==y1 && m < m1) || (y==y1 && m==m1 && d1 > 1) {continue;}
            if diff % 7 == 0 {ans += 1;}
        }
    }
    return ans;
}

#[cfg(test)]
fn is_leap(y:usize) -> bool {
    if y % 400 == 0 {return true;}
    if y % 100 == 0 {return false;}
    if y % 4 == 0 {return true;}
    return false;
}

 

위 코드는 1900년과 가까운 기간의 연도에 대해서는 빠른 시간에 동작한다. 

그러나, 만약 서기 100만 년 1월1일부터 100만년 1월 31일까지를 계산하라고하면, 1900년부터 100만년 1월 1일이 될 때까지 날수를 더해나가야 해서 비효율적이다.

 

실제로 hackerrank 사이트에서는 연도가 $10^{16}$까지를 input으로 하기에, 위 코드로는 통과하지 못한다.

 

좀 다른 방법이 필요하겠다.

 

풀이 2 


1900.1.1일을 기준으로 해서 날수를 세가면서 요일을 계산하는 것이 아니라, 그냥 어떤 Date가 주어지면 해당 일의 요일이 어떻게 되는지를 수식으로 계산하는 함수를 만들어보자. 

그러고 나서, 입력으로 주어지는 두 기간 사이의 매달 1월 1일이 일요인지를 체크하면서 카운트하면 되겠다.

 

어떤 Date가 주어졌을 때, 그날의 요일을 계산하는 식으로는 첼러의 공식(Zeller's Congruence)이 있다.

 

 

이 공식은 수학적으로는 멋지게 보이나, 직관적이지 않고 프로그래머틱하지 않다. 

새롭게 만들어보자.

 

기본 아이디어는 다음과 같다. 

- 서기 1년 1월 1일은 월요일이다. 월요일을 1로 하자. 화요일은 2, ..., 토요일은 6, 일요일은 0

- 2024.8.12일의 요일을 구한다고 생각해 보면,

- 먼저, 2024.1.1일의 요일을 구한다. 매년 365일 혹은 366일이니, 윤년만 고려하면 쉽게 2024.1.1일까지의 날수를 구할 수 있고, 이것을 모듈레이션 7을 하면, 요일을 구할 수 있겠다.

- 그다음은 2024.1.1일부터 2024.8.1일까지의 날수를 구하고, 그로부터 요일을 구하고,

- 마지막으로 2024.8.1일부터 2024.8.12일까지의 날수를 구하고, 그로부터 요일을 구한다.

 

위 아이디어를 그림으로 표현하면 아래와 같다.

(1) 연도 간 요일 구하기

일반적으로 365일이고, 365%7=1이기에, 서기 1년 1월 1일에서 y 년 1월 1일의 요일은 $y % 7$이다.

 

예를 들어 서기 3년 1월 1일의 요일을 생각해 보면, 3년 1월 1일까지의 날 수는 2 * 365 

따라서 구하는 요일은 (2 * 365 + 1) % 7 = 731 % 7 = 3 = 수요일

위 계산식 (2 * 365 + 1) % 7을 보면, 

  • 앞부분 2는 (y - 1)을 한 값이다. 서기 1년에서 서기 3년까지니깐 (3-1)이 된 것이다. yy=y-1로 놓자.
  • 365 % 7 = 1이기에 (2 * 365 + 1) % 7 = (2 * 1 + 1) % 7 이다.
    여기서 2는 (y-1)이기에, 다시 쓰면, ( yy * 1 + 1) % 7 = (yy+1) % 7 

이처럼 윤년이 아닌 경우는 y % 7만을 하면, 해당 연도 y의 1월 1일 요일이 된다.

 

그러나, 윤년의 경우는 1년이 366일이어서 1을 더 더해줘야 한다. 

윤년은, 4년, 8년, 12년처럼 4의 배수의 해에 해당하니, 계산하는 연도 사이에 4의 배수 연도가 있으면 +1을 해준다.

 

근데, 100의 배수의 해는 윤년이 아니니 -1을 해준다. 그리고 400의 배수가 되는 해는 다시 윤년이니 +1을 해준다.

let yy = y - 1; //if y=4 --> have to consider only for 1~3
ans = (yy + (yy/4) - (yy/100) + (yy/400) + 1) % 7;

 

(2) 월에 대한 요일 구하기

어떤 해의 1/1일이 요일 w일 때 m월 1일까지의 요일차이는, 1/1일부터 m/1일까지의 일수를 diff라고 하면 (w + diff) % 7이다.

예를 들어, 어떤 해의 1/1일이 1(월요일)이라고 하면, 2/1일은 1월에 있는 31일이 지난 것이기에 1+31%7=4(목요일)이 된다.  

 

diff는 배열로 만들어서 사용하면 편하겠다.

 

윤년의 경우는, 2월이 28이 아니고 29일이기에, 윤년일 때 3월부터 12월까지는 (w + diff + 1) % 7로 계산해야 한다.

//2. month: yyyy/mm/1, if 2024/7/1 -> 1
let month = [0,0,31,59,90,120,151,181,212,243,273,304,334]; //x,x,Jan,Feb,Mar,...,Nov,0
ans = (ans + month[m]) % 7; 
if (y%400==0 || (y%4==0 && y%100!=0)) && m > 2 {ans += 1;}

 

(3)일 수에 의한 요일 계산

어떤 달의 1일의 요일이 w라고 하면, 해당 월의 d일의 요일은 (w + d - 1) % 7이다.

예를 들어 2024년 7월 1일이 1(월요일)이라고 하면, 7/15일은 (1 + 15 - 1) % 7 = 1(월요일)이다.

//3. day: yyyy/mm/dd, if 2024/7/15 -> 1
ans = (ans + d - 1) % 7;

 

이렇게 구현한 코드는 아래와 같고, 이렇게 하면 hackerrank 사이트 문제도 통과한다.

#[cfg(test)]
fn answer1((y1,m1,d1):(usize,usize,usize), (y2,m2,_d2):(usize,usize,usize)) -> usize{
    let mut ans:usize = 0;    
    'outer: for y in y1..=y2{
        for m in 1..=12 {
            if y==y1 && (m<m1 || (m==m1 && d1>1)) {continue;}
            if y==y2 && m>m2 {break 'outer;}
            if cal_weekaday1(y,m,1)==0 {ans += 1;}
        }
    }
    return ans;
}

#[cfg(test)]
// Monday: 1, ... Sat: 6, Sunday: 0
fn cal_weekaday1(y:usize, m:usize, d:usize) -> usize {
    let mut ans:usize;

    //1. year: yyyy/1/1, if 2024/1/1 -> 1
    // 365 % 7 = 1, so y*365 = y
    let yy = y - 1; //if y=4 --> have to consider only for 1~3
    ans = (yy + (yy/4) - (yy/100) + (yy/400) + 1) % 7;
    

    //2. month: yyyy/mm/1, if 2024/7/1 -> 1
    let month = [0,0,31,59,90,120,151,181,212,243,273,304,334]; //x,x,Jan,Feb,Mar,...,Nov,0
    ans = (ans + month[m]) % 7; 
    if (y%400==0 || (y%4==0 && y%100!=0)) && m > 2 {ans += 1;}

    //3. day: yyyy/mm/dd, if 2024/7/15 -> 1
    ans = (ans + d - 1) % 7;

    return ans;
}

 

풀이 3


어떤 날자에 대한 요일 계산을 위에서 얘기한 첼러의 공식을 이용해서 해보자.

 

2024년 7월 15일을 가지고 공식을 설명해 보겠다.

 

  • h는 요일을 나타내는 정수이다. (0:토, 1:일, 2:월, 3:화, 4:수, 5:목, 6:금)  --> 위 풀이 1과 2에서와 다른 것에 유의
  • q는 날자(day of month). 이 예제에서는 q=15
  • m은 월을 나타내는데, 1월과 2월은 13, 14로 표현 (3:3월, 4:4월, ..., 12:12월, 13:1월, 14:2월). 여기서 m=7
  • year는, 월이 1월과 2월의 경우는 year - 1을 하고, 나머지 달의 경우는 그대로 year사용한다. 예를 들어 2024년 1월 1일을 구할 경우는  year = 2023이고 m=13이 된다.
  • K = year % 100. 2024년의 경우 K = 2024 % 100 = 24
  • J = floor(year / 100)  2024의 경우 J = floor(2024/100) = 20

공식이 복잡해 보여도, 사실은 풀이 2에서 사용한 논리와 똑같다. 

날자에 해당하는 q에 의한 요일 계산, 월에 대한 요일계산에 해당하는 13(m+1)/5, 연도에 의한 계산인 K + K/4 + J/4 - 2J

 

월에 대한 계산인 13(m+1)/5는, 각 월의 일수를 % 7한 것을 가지고, 해당 월까지의 일수합이 계산값이 floor를 하면 정확하게 떨어지도록 나누기 5를 교묘하게 한 것이다.

여하튼 위 공식을 구현하면 아래와 같다.

fn cal_weekaday2(year:usize, month:usize, day:usize) -> usize {
    let q = day;
    let m = if month <= 2 {month+12} else {month};
    let k = if month <= 2 {(year-1) % 100} else {year % 100};
    let j = if month <=2 {(year-1) / 100} else {year/100};

    let mut t;
    t = q + (13*(m+1)/5);
    t += k;
    t += k/4;
    t += j/4;
    t -= (2*j) % 7;
    t = (t-1) % 7;    
    
    return t;    
}

공식의 항목을 풀어서 계산한 것은, 마지막 부분은 (2*j)를 빼는 부분이 있는데, 이에 의해 t가 음수가 되는 시점이 발생할 수 있고, 이렇게 되면 모든 변수를 usize가 아닌 i64로 바꿔서 계산해야 오류가 발생하지 않는다. 해서, 그냥 usize를 사용하기 위해서 스텝별로 계산하면서 (2*j) % 7을 해버림으로써 t에서 빼지는 값을 작게 만들어서 t가 음수가 되지 않도록 했다.

 

총평


간단하지만, 7을 법으로 한 계산을 생각해보게 하는 괜찮은 문제.

hackerrank에서는 year를 $10^{16}$까지 문제의 입력값으로 주기 때문에, 풀이 2에서와 같이 수식으로 요일을 계산하지 않는 한, 시간 내에 답을 내기가 쉽지 않다.

 

전체 소스코드


p19.rs 

#[test]
fn test(){
    assert_eq!(18, answer((1900,1,1), (1910,1,1)));
    assert_eq!(35, answer((2000,1,1), (2020,1,1)));
    assert_eq!(171, answer((1901,1,1), (2000,12,31)));
    assert_eq!(0, answer((1900,7,31), (1901,1,1)));
}

#[cfg(test)]
fn answer((y1,m1,d1):(usize,usize,usize), (y2,m2,_d2):(usize,usize,usize)) -> usize{
    let month = [0,31,28,31,30,31,30,31,31,30,31,30,31];
    
    let mut ans:usize = 0;
    let mut diff:usize = 1;
    'outer: for y in 1900..=y2{
        for m in 1..=12 {
            if y == y2 && m >= m2 { break 'outer;}
            diff += if is_leap(y) && m==2 {month[m]+1} else {month[m]};
            
            if y < y1 || (y==y1 && m < m1) || (y==y1 && m==m1 && d1 > 1) {continue;}
            if diff % 7 == 0 {ans += 1;}
        }
    }
    return ans;
}

#[cfg(test)]
fn is_leap(y:usize) -> bool {
    if y % 400 == 0 {return true;}
    if y % 100 == 0 {return false;}
    if y % 4 == 0 {return true;}
    return false;
}

//-----------------

#[test]
fn test1(){
    assert_eq!(6, cal_weekaday2(2000, 1, 1));
    assert_eq!(1, cal_weekaday2(2024, 7, 15));
    assert_eq!(1, cal_weekaday2(1900, 1, 1));
    assert_eq!(6, cal_weekaday2(1908, 2, 1));

    assert_eq!(6, cal_weekaday2(2000, 1, 1));
    assert_eq!(2, cal_weekaday2(2000, 2, 1));
    assert_eq!(3, cal_weekaday2(2000, 3, 1));

    assert_eq!(18, answer1((1900,1,1), (1910,1,1)));
    assert_eq!(35, answer1((2000,1,1), (2020,1,1)));
    assert_eq!(171, answer1((1901,1,1), (2000,12,31)));

    assert_eq!(6, answer1((1943,1,1), (1946,12,31)));
    assert_eq!(10, answer1((1995,1,1), (2000,12,31)));
    
    assert_eq!(0, answer1((1900,7,31), (1901,1,1)));
}

#[cfg(test)]
fn answer1((y1,m1,d1):(usize,usize,usize), (y2,m2,_d2):(usize,usize,usize)) -> usize{
    let mut ans:usize = 0;    
    'outer: for y in y1..=y2{
        for m in 1..=12 {
            if y==y1 && (m<m1 || (m==m1 && d1>1)) {continue;}
            if y==y2 && m>m2 {break 'outer;}
            if cal_weekaday1(y,m,1)==0 {ans += 1;}
        }
    }
    return ans;
}

#[cfg(test)]
// Monday: 1, ... Sat: 6, Sunday: 0
fn cal_weekaday1(y:usize, m:usize, d:usize) -> usize {
    let mut ans:usize;

    //1. year: yyyy/1/1, if 2024/1/1 -> 1
    // 365 % 7 = 1, so y*365 = y
    let yy = y - 1; //if y=4 --> have to consider only for 1~3
    ans = (yy + (yy/4) - (yy/100) + (yy/400) + 1) % 7;
    

    //2. month: yyyy/mm/1, if 2024/7/1 -> 1
    let month = [0,0,31,59,90,120,151,181,212,243,273,304,334]; //x,x,Jan,Feb,Mar,...,Nov,0
    ans = (ans + month[m]) % 7; 
    if (y%400==0 || (y%4==0 && y%100!=0)) && m > 2 {ans += 1;}

    //3. day: yyyy/mm/dd, if 2024/7/15 -> 1
    ans = (ans + d - 1) % 7;

    return ans;
}

#[cfg(test)]
// Monday: 1, ... Sat: 6, Sunday: 0
fn cal_weekaday2(year:usize, month:usize, day:usize) -> usize {
    let q = day;
    let m = if month <= 2 {month+12} else {month};
    let k = if month <= 2 {(year-1) % 100} else {year % 100};
    let j = if month <=2 {(year-1) / 100} else {year/100};

    let mut t;
    t = q + (13*(m+1)/5);
    t += k;
    t += k/4;
    t += j/4;
    t -= (2*j) % 7;
    t = (t-1) % 7;    
    
    return t;    
}

 

반응형