[editorial] TCO09 Qual 2

  • User Avatar
    Taeyoon_Lee

    11등한 기념으로(형들이 쓰래서) 에디토리얼을 써보겠습니다.
    qual2.PNG
    250, 500, 1000으로 구성된 set이었는데, 그럭저럭 알맞은(?) 난이도였다고 생각합니다.
    예상외의 인물이 1등을 했는데, 저도 1등 한번 해보고 싶어요.(?)

    Easy (250 pts.)

    문제 설명

    n*m 격자로 나뉘어져 있는 나무판이 있습니다.
    각 격자는 검정(B)색이나 흰(W)색이 칠해져 있습니다.
    우리는 8*8 크기로 나무판을 잘라서 체스판을 만들 것입니다.
    체스판은 인접한 모든 격자가 서로 다른 색이어야 합니다.
    그래서 나무판을 자른 후에, 각 격자에 색칠을 다시해야 하는데,
    색칠해야 되는 칸이 최소가 되도록 자르면, 몇 칸을 색칠하면 될까요?
    250답게 brute force로 해결할 수 있습니다.
    가능한 체스판은 아래와 같이 두종류가 있습니다.
    ~~~ text
    WBWBWBWB

    BWBWBWBW
    BWBWBWBW WBWBWBWB
    WBWBWBWB BWBWBWBW
    BWBWBWBW WBWBWBWB
    WBWBWBWB BWBWBWBW
    BWBWBWBW WBWBWBWB
    WBWBWBWB BWBWBWBW
    BWBWBWBW WBWBWBWB
    ~~~
    두가지 판을 가능한 모든 위치에 대응해보면서
    처음 칠해진 칸과 다른 칸의 개수를 세어보면 됩니다.
    ~~~ cpp

    #define FOR(i,a,b) for( int i=(a); i<(b); ++i)
    #define REP(i,n) for(int i=0; i<(n); ++i)
    #define SZ(X) (int)(X).size()
    class RepaintTheChessboard {
    public:
    int minimumChanges(vector dt) {
    int n=SZ(dt),m=SZ(dt[0]);
    int dp=234234234;
    REP(i,n-8+1) {
    REP(j,m-8+1) {
    int su=0;
    FOR(y,i,i+8)
    FOR(x,j,j+8) {
    if ((y+x)%2==0 && dt[y][x]=='W') su++;
    if ((y+x)%2==1 && dt[y][x]=='B') su++;
    }
    dp=min(dp,su);
    dp=min(dp,64-su);
    }
    }
    return dp;
    }
    };

    </spoiler>
    
    ### Medium (500 pts.)
    
    #### 문제 설명
    1페이지부터 N페이지까지 적힌 책이 있습니다. 이 책에 0부터 9까지 각각의 digit는 몇번 등장할까요?
    
    <spoiler>
    N이 10억이기 때문에 간단한 O(n) 방법은
    시간 제한에 걸리게 됩니다. 덕분에 첼 하나 했죠. :)
    제 방법은 "10*N의 길이" 정도의 반복으로 풉니다. (결국 O(logN)이죠.)
    다른 방법도 있을 것 같은데,
    이미 푼 문제는 더 연구하지 않는 게으른 성격 탓에
    찾아보진 않았네요.
    예를 들어서 설명해볼게요.
    123부터 456까지 각각의 디지트의 개수를 구해보죠.
    우선 숫자를 늘어놓아 볼까요?
    ~~~ text
    123
    124
    125
    .
    .
    .
    454
    455
    456
    

    그리고 위 아래에서 숫자를 하나씩 처리합니다. (답에 누적시킵니다.)
    ~~~ text
    123
    124
    .
    .
    129
    130
    131
    .
    .
    448
    449
    450
    451
    452
    .
    .
    ~~~
    시작 숫자의 1의 자리가 0이면 계산을 잠시 멈추고,
    마지막 숫자의 1의 자리가 9면 역시 계산을 잠시 멈춥니다.
    그리고 위에서부터 숫자를 10개씩 묶어 볼게요.
    0으로 시작해서 9로 끝나니까, 딱 맞게 묶이겠죠?
    그러면 우선 1의 자리부터 처리합시다.
    ~~~ text
    130
    131
    .
    .
    448
    449
    ~~~
    0~9가 각각 (44-13+1)개가 있습니다. 계산하시구요.
    1의 자리를 처리하고나면, 문제는
    13부터 44까지 등장하는 각각의 디지트를 세는 문제로 바뀝니다.
    물론 곱하기 10을 해서 답을 구해야 겠죠?
    ~~~ cpp

    typedef vector VI;
    #define REP(i,n) for(int i=0; i<(n); ++i)
    VI dp;
    void ret(int ka,int kb)
    {
    while (ka>0) {
    dp[ka%10]+=kb;
    ka/=10;
    }
    }
    class PageNumbers {
    public:
    vector getCounts(int N) {
    int a=1,b=N,c=1;
    dp = VI(10,0);
    while (a<=b) {
    while (a%10>0 && a<=b)
    ret(a++,c);
    if (a>b)
    break;
    while (b%10>0 && a<b)
    ret(b--,c);
    REP(i,10)
    dp[i]+=(b/10-a/10)*c;
    ret(b--,c);
    a/=10;
    b/=10;
    c*=10;
    }
    return dp;
    }
    };

    </spoiler>
    ### Hard (1000 pts.)
    
    #### 문제 설명
    매초 1씩 카운트가 증가하는 N자리 숫자 디지털 카운터가 있습니다.
    카운터는 숫자가 계속 돕니다. 10^N - 1 다음에 0이 됩니다.
    (자리수는 계속 유지되어 999 다음에는 000이 됩니다.)
    0부터 9까지 각각의 숫자는 7개 이하의 선분으로 이루어 집니다.
    0은 6개, 1은 2개, 2는 5개... 생략 {6,2,5,5,4,5,6,3,7,5}
    현재 카운터에 적힌 숫자가 주어집니다.
    현재 적힌 숫자를 쓰는데 이용된 선분의 합과,
    선분의 합이 같은 숫자가 나오려면 몇 초를 기다려야 될까요?
    <spoiler> 제가 푼 방식보다 훨씬 좋은 방법이 있는 것 같은데,
    역시 이미 pass했기 때문에, 'ㅁ' 굳이 공부하진 않아서,
    제 방법을 써보겠습니다.
    시간에 쫓기면서 푸느라 코드가 더러워서,
    여기에 제 코드 전체를 쓰진 않겠구요,
    주요 부분만 보여드리겠습니다.
    우선 다이나믹 테이블을 하나 만듭니다.
    dy[i][j][k] = 길이 i에서, 선분이 j개 남았을 때, 제일 앞에 k라는 숫자가 올 수 있는가? (있다면 1, 아니면 0)
    dy[i][j][k] = dy[i-1][j-su[k]][0~9] 중에 1이 있으면 1, 아니면 0
    ~~~ cpp
    
    #define FOR(i,a,b) for( int i=(a); i<(b); ++i) 
    #define REP(i,n) for(int i=0; i<(n); ++i) 
    #define FORD(i,a,b) for( int i=(a); i>(b); --i) 
    int dy[20][110][15]={0,}; 
    int su[10]={6,2,5,5,4,5,6,3,7,5}; 
        REP(i,10) dy[1][su[i]][i]=1; 
        FOR(i,2,20) 
          FOR(j,1,110) 
            REP(k,10) if (j-su[k]>=0) 
              REP(q,10) if (dy[i-1][j-su[k]][q]) 
                dy[i][j][k]=1; 
    
    

    이걸 구해놓으면 뒷자리부터 하나씩 올려가면서
    계산을 해볼 수 있습니다.
    예를 들어 현재 숫자가 123456이라고 하면
    끝에 6을 7, 8, 9로 하나씩 바꿔가면서 가능 여부를 검사합니다.
    6을 쓰는데 이용되는 선분의 수가 6이니까
    dy[1][6][7], dy[1][6][8], dy[1][6][9] 중에 1이 있나 검사를 해보고, 있으면 그렇게 바꾸면 됩니다.
    하지만 아쉽게도 전부 1이 아닙니다.
    그러면 다음 자리로 옮겨와서 5를 하나씩 올려봅니다.
    56을 쓰는데 이용된 선분의 수는 11개니까
    dy[2][11][6], dy[2][11][7], dy[2][11][8], dy[2][11][9]를 찾아보겠죠.
    dy[2][11][6]이 1이겠네요. 그럼 5를 6으로 바꿔주고,
    그 뒷자리는 무조건 가능한 가장 작은 수로 써줍니다. 역시 dy배열을 참고해서 해야 겠죠?
    dy[1][5][2]가 1일 테니, 처음에 "56"이었던 숫자가 "62"로 바뀌면 되겠군요.
    ~~~ cpp

    #define FOR(i,a,b) for( int i=(a); i<(b); ++i)
    #define REP(i,n) for(int i=0; i<(n); ++i)
    #define FORD(i,a,b) for( int i=(a); i>(b); --i)
    // dt[0,n-1] 에 각 자리의 숫자가 들어가 있습니다.
    FORD(i,n-1,-1) {
    int bu,sum;
    bu=dt[i];
    sum=0;
    FOR(j,i,n) sum+=su[dt[j]]; // 바꿀 숫자들의 선분 합을 구합니다.
    while (dt[i]<9) { // 9일 때까지 바꿔 봅니다.
    dt[i]++;
    if (dy[n-i][sum][dt[i]]) { // 가능하다면, 그렇게 써주고
    sum-=su[dt[i]];
    i++;
    while (i<n) { // 뒷자리는 가능한 제일 작은 수로 채웁니다.
    REP(j,10)
    if (dy[n-i][sum][j]) {
    sum-=su[j];
    dt[i]=j;
    break;
    }
    i++;
    }
    // 현재 바뀐 숫자에서 원래 숫자를 빼서 답을 구합니다.
    // 생략했습니다.
    return ret;
    }
    }
    dt[i]=bu; // 9일 때까지 바꿔 보았는데, 가능한 경우가 없으면 그냥 다시 돌려 놓습니다.
    }

    만약 입력이 "11111"나 "71111" 이런 형태면 "99999"가 될 때까지 가능한 답을 찾을 수가 없습니다.
    그렇다면 어떻게 하면 될까요?
    간단합니다. (하지만 더럽습니다.)
    "00000"이 될 때까지 시간이 흘렀다고 생각하고, 그때까지 걸린 시간을 구해놓고, 답을 구하세요.
    이때는 전체 성냥 개수를 미리 저장해서 계산해야 되는데..
    자세한 코드는 적지 않을게요.</spoiler>
    이해가 안 되는 부분 있으면 리플로 알려주세요~
    여기까지 읽어주신 분, 감사합니다. <img src="http://algospot.com/zbxe/modules/editor/components/emoticon/tpl/images/animated/animate_emoticon%20%2832%29.gif" alt="emoticon">
    <div>[이 글은 과거 홈페이지에서 이전된 글입니다. <a href="http://algospot.andromeda-express.com/zbxe/editorial/56105">원문보기</a>]</div>
    

    17년 전
6개의 댓글이 있습니다.
  • User Avatar
    astein

    만약 입력이 "11111"나 "71111" 이런 형태면 "99999"가 될 때까지 가능한 답을 찾을 수가 없습니다.
    그렇다면 어떻게 하면 될까요?
    --> tricky 한 방법이 있는데,
    모든 입력의 앞에 "2"를 붙여주고 실행합니다. ( lit[2] = lit[3] 임을 이용 )
    "211111"의 답은 "311111"이 되고, "271111"의 답은 "311117"이 되는 식이지요.


    17년 전 link
  • User Avatar
    Taeyoon_Lee

    우왕ㅋ 좋은 방법이네요~


    17년 전 link
  • User Avatar
    JongMan

    하드에서 더 좋은 방법은, 에디토리얼에도 나온 방법이지만, 두 가지 경우로 나눠 푸는 것인 것 같아요.
    1. 주어진 숫자를 A 라고 할 때, A < B 에 대해서 count(A) == count(B) 인 경우
    a. A 와 B 가 첫 자리에서 달라질 때: B 의 해당 자리 수는 A 의 해당 자리수보다 커야 합니다.
    b. A 와 B 가 두번째 자리에서 달라질 때: B 의 해당 자리 수는 A 의 해당 자리수보다 커야 합니다.
    c.
    d. ...
    2. B < A 인 경우: 99999...9 를 지나 wraparound 한 경우죠.
    두 경우 모두, smallest[i][j] = i개의 segment 를 가지고 만들 수 있는 j자리의 수 중 가장 작은 것은? 이라는 점화식을 풀면 쉽게 풀 수 있죠. 'X 보다 lexicographically larger 중 가장 작은 것 구하기' 스타일의 문제를 풀 수 있는 일반적 패턴인데.. 'X 와 Y 가 달라지는 첫 번째 위치' 를 찾는거죠. 그 전은 X 와 똑같아야 하고, 그 이후는 가능한 가장 작은 수일 테니까요.


    17년 전 link
  • User Avatar
    astein

    음.. 제가 연습할 때 푼 방법도 위에서 JM님이 설명해 주신 방법과 완전히 같네요 :$
    wraparound인 경우는 첫번째 댓글에 달아뒀던 것처럼 string의 앞에 "2"를 붙여주면 해결이 되고요.
    " smallest[i][j] = i개의 segment, j자리의 수 중 가장 작은 것은? " 이라는 문제는 greedy 로 해결 가능한 문제입니다.
    k 자리의 digit을 채우는 segment는 최소 2k개, 최대 7k개임을 알 수 있기 때문이죠. 앞자리를 0 ~ 9 까지 채워넣어가면서,
    k-1 자리의 digit에서 사용할 수 있는 segment가 2(k-1)이상, 7(k-1)이하인 경우는 앞자리를 고정시킬 수 있으니까요 :3


    17년 전 link
  • User Avatar
    Being

    오래 전 글이라 깨진 곳이 많아서 손봤습니다. :) 지금 생각해보면, 미듐에 대해 1부터 1345까지의 답을 구한다고 하면, 이 답이 [1, 133] 까지의 답에 10배한 것에다가 1340 1341 1342 1343 1344 1345만 더 고려하면 되는 것 아닌가 싶네요.


    12년 전 link
  • User Avatar
    Being

    아, 1, 2, 3, 4, 5, 6, 7, 8, 9 도 생각해야겠군요.


    12년 전 link
  • 정회원 권한이 있어야 커멘트를 다실 수 있습니다. 정회원이 되시려면 온라인 저지에서 5문제 이상을 푸시고, 가입 후 7일 이상이 지나셔야 합니다. 현재 문제를 푸셨습니다.