[editorial] 2007년 서울 온사이트 본선 J번 Number

  • Kyungryeol
    Kyungryeol

    죄송합니다만, 저는 아직 제 풀이가 완벽한 해법이라고 장담하지 못한다는 점을 미리 밝힙니다.
    (틀린답이 나오지는 않겠지만 최악의 경우에 대한 시간복잡도를 잘 모르겠습니다)
    제가 원래 "일단 해보자" (잘못됐다는건 알지만 고치기 힘든) 는 마인드를 갖고 있어서,
    혹시나 하는 마음으로 해봤는데 시간내에 나와주네요.
    문제는 굉장히 간단합니다.
    주어진 두개의 set, S와 T에 속하는 원소를 각각 s와 t라고 할때,
    이들을 서로 pair로 만들어서 cost를 가장 작게 하는건데요,
    cost는 모든 pair의 abs(s-t) 의 합이며, S와 T의 모든 원소가 pair에 속해야 합니다.

    N 제한 50,000을 생각하지 않고 떠오르는 가장 간단한 방법은 Dynamic Programming입니다.
    A[i][j] = set S 의 i번째 원소까지, set T의 j번째 원소까지 match시켰을때 최소 cost의 합 이라고 정의하면
    A[i][j] = min (A[i-1][j-1], A[i-1][j], A[i][j-1]) + abs(S[i] – T[j]);
    가 되겠습니다. 이는 S와 T가 increasing order로 들어온다는 조건이 있기 때문에 가능하죠.
    하지만 이 방법은 시간복잡도가 O(N^2) 이 되겠습니다. 막막하죠.

    제 풀이법은 이 DP의 시간복잡도를 약간 개선시킨 것입니다.
    set S 의 모든 원소들에 대해서 set T의 모든 원소를 보지 않고, “가능성이 있는 원소” 들만 고려하였습니다.
    예를 들어보겠습니다.
    입력으로 S = {4, 5, 7, 9}, T = {1, 3, 20, 30} 가 주어졌습니다.
    여기서 S의 5는 “절대” T의 1과 match되지 않습니다. 왜냐하면,
    abs(4-1) + abs(5-1) = 3 + 4 = 7,
    abs(4-1) + abs(5-3) = 3 + 2 = 5 이기 때문입니다.
    즉, (4,1), (5,3) pair로 만들었을때 cost가 5인데, 그보다 더 cost가 큰 (4,1) (5,1) pair를 만들 필요가 없다는 말이죠.
    만약에 T의 두번째 원소가 3이 아니라 10이었다면, 5는 1과 pair가 될 수도 있을 것입니다.

    아시겠나요?

    그럼 이제 간략한 소스코드 설명 들어가겠습니다.

    define은 다음과 같이 되어있습니다.

    #define For(i,n) for(int i=0, _n=(n);i<_n;++i)
    #define For2(i,a,b) for(int i=(a), _n=(b);i<_n;++i)
    

    아래의 소스코드는 모든 S의 i번째 원소에 대하여 그 원소와 “pair가 될 수 있는” 원소들의 범위를 구하는 것입니다.

    a[i]는 set S의 i번째 원소이며, b[i]는 set T의 i번째 원소 입니다.
    n1은 set S의 원소 갯수이며, n2는 set T의 원소 갯수 입니다.
    c[i][0]은 set S의 i번째 원소와 pair가 될 수 있는 set T의 원소들 가운데 lower bound의 index를 가리키며,
    c[i][1]은 set S의 i번째 원소와 pair가 될 수 있는 set T의 원소들 가운데 upper bound의 index를 가리킵니다.
    여기까지의 시간복잡도는 겉보기와 달리 실제로 따져보면 O(N)이 되겠습니다.

    int t = c[0][0] = 0;
    For2(i,1,n1) 
    {
        int t2 = 0;
        int minn = abs(a[i]-b[t]);
        c[i][0] = t;
        For2(j,t+1,n2)
        {
          if(t2 + abs(a[i]-b[j]) > minn) break;
          minn = t2 + abs(a[i]-b[j]);
          t2 += abs(a[i-1]-b[j]);
          c[i][0] = j;
        }
        t = c[i][0];
    }
    t = c[n1-1][1] = n2-1;
    for(int i = n1-2; i >= 0; i--)
    {
        int t2 = 0;
        int minn = abs(a[i]-b[t]);
        c[i][1] = t;
        for(int j = t-1; j >= 0; j--)
        {
          if(t2 + abs(a[i]-b[j]) >= minn) break;
          minn = t2 + abs(a[i]-b[j]);
          t2 += abs(a[i+1]-b[j]);
          c[i][1] = j;
        }
        t = c[i][1];
    }
    

    그리고 나서 DP를 돌리는 과정은 다음과 같습니다.

    int t1 = 0, t2 = 1;
    For(i,n2) d[0][i] = d[1][i] = -1;
    d[t2][0] = abs(a[0]-b[0]);
    For2(i,1,c[0][1]+1) d[t2][i] = d[t2][i-1] + abs(a[0]-b[i]);
    For2(i,1,n1) 
    {
        t2 = t1; t1 = 1 - t1;
        if(c[i][0]) d[t2][c[i][0]] = d[t1][c[i][0]-1];
        if(d[t2][c[i][0]] == -1 || d[t1][c[i][0]] != -1 && d[t1][c[i][0]] < d[t2][c[i][0]]) d[t2][c[i][0]] = d[t1][c[i][0]];
        d[t2][c[i][0]] += abs(a[i]-b[c[i][0]]);
        For2(j,c[i][0]+1,c[i][1]+1)
        {
          d[t2][j] = d[t1][j-1];
          if(d[t2][j] == -1 || d[t2][j-1] != -1 && d[t2][j-1] < d[t2][j]) d[t2][j] = d[t2][j-1];
          if(d[t2][j] == -1 || d[t1][j] != -1 && d[t1][j] < d[t2][j]) d[t2][j] = d[t1][j];
          d[t2][j] += abs(a[i]-b[j]);
        }
        For2(j,c[i-1][0],c[i-1][1]+1) d[t1][j] = -1;
    }
    

    무식하게 생각하면 DP를 구현하기 위해서 O(N^2)의 공간이 필요하지만 아주 조금만 생각하면 O(N)짜리 배열 두개만이 필요하다는 것을 알 수 있을 것입니다. d[0]과 d[1]이 그 두개의 dynamic table이 되겠습니다.
    제 해법이 완벽하지 않다고 글의 서두에 밝힌 이유는 바로 이 부분에 대한 시간복잡도를 제대로 구할 수 없었기 때문입니다.

    이상으로 웬지 허무한 J번 editorial 이었습니다.

    과연 이 알고리즘이 정확한 해법일까요??

    [이 글은 과거 홈페이지에서 이전된 글입니다. 원문보기]


    16년 전
4개의 댓글이 있습니다.
  • Taeyoon_Lee
    Taeyoon_Lee

    lower bound와 upper bound의 index차가 logN에 비례한다면(아니면 상수에) =_=맞지 않을까요?


    16년 전 link
  • Kyungryeol
    Kyungryeol

    반례를 결국 발견하고 말았습니다.
    S가 1부터 50,000까지, T가 50,001부터 100,000 까지 있다면,
    저의 알고리즘은 N^2을 돌면서 시간을 초과해버립니다.
    좀 더 근사한 방법을 생각해봐야겠군요.


    16년 전 link
  • 진영
    진영

    O(N) 1차원 Dynamic에 풀리는 문제이더군요. (정렬된 입력을 가정)
    몇 번 IRC에서 '풀었다'고 낚시도 했었습니다만..
    이리저리 풀어보고 논문도 찾아보다 결국 해결했습니다.
    이로써 2007년 리저널 문제는 모두 풀린 것 같네요!
    시간이 되면 조만간 풀이를 정리해서, 또는 논문을 찾아보고 올리도록 하겠습니다. ^^


    16년 전 link
  • 김우현
    김우현

    자료를 찾아보니
    비슷한 종류의 문제가 정형화된 문제로 다뤄지는 것 같더라구요.
    S와 T를 Cover하는 Edge를 어떻게 정하는가에 따라서
    many-to-one, many-to-many, one-to-one등으로 나누네요.
    아래논문제서는 many-to-one mathcing을 정렬된 입력에 대해서 O(N)으로 matching 찾지만.
    응용하면 될 것 같기도 합니다..
    J문제는 아래 문서에서다루는 many-to-many문제에 해당되는것 같은데요.
    화이팅이요 (-:

    http://john2.poly.edu/papers/jcb05/paper.pdf


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