관련 코드는 github에서 찾아볼 수 있다.


여기서는 잘 알려진 사실들부터 시작해서, 나중에 중요해질 수학적 사실들을 다룬다.


사실 1단원과 2단원 앞 유클리드 알고리즘만 알아도 퍼플/오렌지에 영향은 없다.


잘 알려진 사실들은, 매우 간단하게만 설명하고 스킵하자. 이미 좋은 자료/블로그가 많으니, 이를 참고하는 것 추천.


다음 사실은 유명하다 : 정수 $a, b$가 있고 $b \neq 0$일 때, $a = bq + r$이고 $0 \le r < |b|$인 정수 $q, r$이 유일하게 존재한다.

이때, $r$을 "$a$를 $b$로 나눈 나머지"라고 정의한다. 만약 $r = 0$인 경우, $a, b$는 서로 약수/배수 관계에 있다고 하며, $b|a$라 쓴다.

이를 통해서 공약수/공배수 개념을 정의할 수 있으며, 나아가 최대공약수/최소공배수의 개념을 정의할 수 있다.


자연수 $n \ge 2$가 각 $2 \le k < n$에 대하여 $k \nmid n$을 만족하면, $n$을 소수라고 부른다. 소수가 아니면 (1 제외) 합성수라고 부른다. 

이를 통해서 소인수분해라는 개념을 정의할 수 있으며, 이는 유일하다는 것이 잘 알려져 있다.

소인수분해를 통해서, 자연수의 약수의 개수와 약수들의 합 등을 계산할 수 있음은 교육과정에도 있는 내용이다.


만약 두 정수 $a, b$가 $n$으로 나눈 나머지가 같다면, $a \equiv b \pmod{n}$이라고 쓴다. 이는 $n|(a-b)$와 같은 뜻이다.

또한, $a$를 $n$으로 나눈 나머지를 $a \pmod{n}$이라고 쓰기도 한다. 다음은 간단 Exercise.

  • 정수 $a, b, n$이 $a \equiv b \pmod{n}$이고, $r$이 정수인 경우, 다음이 모두 성립함을 보여라.
  • $a + r \equiv b+ r \pmod{n}$, $a - r \equiv b - r \pmod{n}$, $ar \equiv br \pmod{n}$.

즉, mod 연산을 취하는 과정에서도 평소처럼 사칙연산을 할 수 있다. 

나눗셈이 문제인데, 다음 단원에서 modular inverse가 나오면 나눗셈도 modular inverse를 양변에 곱하는 방식으로 할 수 있다.


이 시점에서, 알고리즘 문제를 몇 개 생각해볼 수 있다. 모두 이미 자료가 많으므로, 빠르고 간단하게 한다.

  • 자연수가 소수임을 어떻게 판별할 것인가?
  • 자연수의 약수의 개수를 어떻게 셀 것인가? 약수를 모두 찾으라고 한다면 어떻게 할 것인가?
  • 자연수의 소인수분해를 어떻게 구할 것인가?
  • 1부터 $n$까지의 소수를 어떻게 셀 것인가/찾을 것인가?

첫 세 문제에 대해서는, $\mathcal{O}(\sqrt{n})$ 알고리즘이 잘 알려져 있다. 핵심 아이디어는,

  • 자연수 $n$이 $2$ 이상 $\sqrt{n}$ 이하 모든 자연수에 의해 나누어떨어지지 않는다면, $n$은 소수다.
  • 이유: 소수가 아니라면 $n = ab$이며 $1  < a , b < n$인 자연수 $a, b$ 존재. 둘 다 $\sqrt{n}$보다 크다면 $ab > n$.

이 핵심 아이디어로 세 문제를 해결해보자.

  • 소수 판별: $2$ 이상 $\sqrt{n}$ 이하 모든 자연수에 대해 나누어떨어짐을 판별하면 끝.
  • 약수 세기/찾기: $a$가 $n$의 약수면 $n/a$ 역시 $n$의 약수. 둘 중 하나는 $\sqrt{n}$ 이하고 하나는 $\sqrt{n}$ 이상이니, $\sqrt{n}$ 이하 약수만 찾으면 모든 약수를 알 수 있다.
  • 소인수분해: $2$ 이상 $\sqrt{n}$ 이하 모든 자연수를 순서대로 보자. 현재 보고 있는 자연수 $i$가 $n$의 약수라면, $n$이 $i$의 배수가 아닐 때까지 $n$에서 $i$를 나누어준다. 나누어주는 횟수를 저장하는 것은 당연. 만약 iteration이 끝났을 때, $n$이 $1$이면 그대로 끝. 아니면 남은 $n$ 역시 소수이며, 기존 $n$의 소인수가 된다. 

마지막 문제는 "에라토스테네스의 체"로 잘 알려져 있는 결과다.

  • 에라토스테네스의 체: 2부터 $n$까지 순서대로 보면서, 합성수인 것을 지워내자. 만약 지금 보는 값 $p$가 지워지지 않았다면, $p$는 소수다. 
  • 이제 $p$의 배수를 ($p$는 빼고) 전부 지워내자. 이걸 반복하면 남는 것은 전부 소수. 예시를 인터넷에서 찾아보면서 이해해보자. 

에라토스테네스의 체는 활용 방법이 굉장히 많다. 다음을 할 수 있는 방법을 생각해보자. 해답은 github에 있다.

  • 각 $2$부터 $n$까지의 자연수에 대하여, "그 자연수가 갖는 가장 작은 소인수"를 계산하는 법
  • 각 $2$부터 $n$까지의 자연수에 대하여, "그 자연수가 갖는 서로 다른 소인수 개수"를 계산하는 법
  • 오일러 파이 함수. $n$의 소인수분해가 $p_1^{e_1} p_2^{e_2} \cdots p_k^{e_k}$일 때, $\phi(n) = n \cdot \frac{p_1 - 1}{p_1} \cdots \frac{p_k-1}{p_k}$라 정의하자. 이 값을 모두 계산하는 법.
  • 조금 더 난이도가 있지만, "약수의 개수"와 "약수의 배수" 역시 전처리 할 수 있다.

여기서 강력한 결과를 하나 얻을 수 있다.

  • 각 $2$부터 $n$까지의 자연수에 대하여, "그 자연수가 갖는 가장 작은 소인수"를 미리 전처리했다고 가정하자.
  • 이제 $2 \le m \le n$인 자연수 $m$의 소인수분해를 원한다고 가정하자. 
  • $m$에서 시작해서, "가장 작은 소인수"를 나누어주는 과정을 반복하자. 한 번에 값이 반 이상 줄어드니, 로그 시간에 소인수분해 끝.

에라토스테네스의 체를 사용하면 $1, 2, \cdots , n$ 각각이 소수인지 전부 빠르게 판별할 수 있다.

하지만 꼭 $1$부터 시작할 필요는 없다. $A+1, A+2, \cdots , A+n$ 각각이 소수인지 빠르게 판별할 수는 없을까?

  • $2$부터 $\sqrt{A+n}$까지의 수를 순서대로 보자. 현재 보고 있는 수를 $k$라고 하자.
  • $A+1, A+2, \cdots , A+n$에 속한 $k$의 배수 중 $k$가 아닌 것을 전부 제거하자. 남는 것은 전부 소수이다.
  • 시간복잡도는 어떻게 될까? 각 $k$에 대하여 제거되는 수의 개수는 $\mathcal{O}(n/k)$개이다.
  • 이를 $k$를 $2$부터 $\sqrt{A+n}$까지 돌면서 더하면, 아래에서 다룰 Harmonic Sequence에 대한 성질에 의해 총 $\mathcal{O}(n \log n)$이 된다.
  • 그러니 $k$에 대하여 for문을 돌리기 위해 필요한 시간 $\mathcal{O}(\sqrt{A+n})$을 더하면, $\mathcal{O}(\sqrt{A} + n \log n)$이라는 결과를 얻는다.
  • 에라토스테네스의 체의 다양한 활용 방법을 이 방식에서도 그대로 적용할 수 있다.


참고할 점은, 위의 에라토스테네스의 체가 하는 일을 $\mathcal{O}(n)$에 하는 방법이 존재한다는 것이다.

  • Linear Sieve라고 하는 테크닉이다. 이를 위해서는 ahgus89님의 이 자료나, 원본이라고 할 수 있는 이 글을 보자.
  • 이후에 다룰 multiplicative function에 대한 글을 읽고 다시 위의 자료를 읽는 것이 이해하기 편할 수 있다.
  • 이 가이드에서 굳이 Linear Sieve를 설명하지 않는 이유는, 필자도 Linear Sieve가 필요할 정도로 시간제한이 빡빡한 문제를 본 적이 없기 때문이다.
  • 이 외에도, 에라토스테네스의 체는 다양한 최적화 방법이 있다. 개인적으로는 필요한 문제를 본 적이 없었다. 필요하면 나쁜 문제라는 것이 내 생각.
  • 당연하지만, 이러한 체 최적화 방법을 알아둬서/팀노트에 넣어서 나쁠 것은 없다. 나중에 관심이 생기면 보는 것으로.


이제 여러 수학적 사실들을 빠르게 다룬다. 중요해지는 때가 온다.

  • [매우 중요] 자연수 $n$에 대하여, $\lfloor n/1 \rfloor, \lfloor n/2 \rfloor, \cdots, \lfloor n/n \rfloor$ 중 서로 다른 것은 $\mathcal{O}(\sqrt{n})$개이며, 효율적인 iteration도 가능. github 참조.
  • 이유가 꽤 아름다우니 설명한다. $\lfloor n/1 \rfloor, \cdots , \lfloor n / \lfloor \sqrt{n} \rfloor \rfloor$까지 서로 다른 것은 $\mathcal{O}(\sqrt{n})$개. (항 개수가 애초에 $\mathcal{O}(\sqrt{n})$개이므로)
  • $\lfloor n / \lfloor \sqrt{n} \rfloor \rfloor, \cdots ,\lfloor n/n \rfloor$까지 서로 다른 것도 $\mathcal{O}(\sqrt{n})$개. (값들이 전부 $\mathcal{O}(\sqrt{n})$ 이하이므로)
  • 위 github 코드의 iteration 방법의 원리가 궁금할 수 있을 것이다. 이것의 대한 설명은 ahgus89님의 이 글 참고.
  • [매우 중요] Harmonic Sequence. 자연수 $n$에 대하여, $1/1 + 1/2 + 1/3 + \cdots + 1/n = \mathcal{O}(\log n)$.
  • 7단원에서 언급하겠지만, 이를 통해서 $1$부터 $n$까지 약수의 개수 합이 $\mathcal{O}(n \log n)$임을 얻는다.
  • 그러므로, 만약 필요하다면, $1$부터 $n$이 갖는 약수들을 std::vector 등 자료구조에 $\mathcal{O}(n \log n)$ 시간에 넣을 수 있다.
  • 더욱 자세한 내용을 알고 싶다면, 7단원에서 "약수를 iterate 하는 것보다 배수를 iterate 하는 것이 쉽다"는 부분을 읽자.
  • Merten's 2nd Theorem. 자연수 $n$에 대하여, $n$ 이하 소수들의 역수의 합은 $\mathcal{O}(\log \log n)$.
  • Merten's 2nd Theorem으로, 에라토스테네스의 체의 시간복잡도를 파악할 수 있을 것이다. $\mathcal{O}( n \log \log n)$.
  • 후반부의 내용에서, 은근슬쩍 자연수 $a, b, c$에 대하여 $\lfloor \lfloor a/b \rfloor / c \rfloor = \lfloor a/(bc) \rfloor$라는 식을 꽤 사용한다.


마지막으로, 모듈로 연산에 대해 잠깐 언급한다. 이 연산은 덧셈, 뺄셈에 비해서는 물론, 곱셈에 비해서도 느린 연산이다.

그러니, 모듈로 연산을 마구잡이로 사용하면 비효율적인 코드가 나오게 될 것이다. 이를 최적화하는 방법을 간단히 소개한다.


모듈로 연산 횟수를 줄이는 법

  • 가능하면 진짜 나머지를 취하지 말고, 모듈러스를 더하고 빼는 것으로 대체한다.
  • 예를 들어, $0 \le a < M$, $0 \le b < M$에 대해, $a+b \pmod{M}$을 계산하기 위해서 모듈로 연산을 쓰지 말자는 것이다.
  • $a+b$를 계산한 다음, $M$보다 큰 경우 $M$을 빼는 것이 훨씬 효율적이다. 쉽고 확실한 최적화 방법이다.

모듈로 연산 자체를 빠르게 하는 방법

  • 모듈로를 가능하면 const로 선언하자. 그러면 상당한 속도 개선을 확인할 수 있다.
  • 이러한 고속화의 원리 중 하나는 Barrett Reduction이다. modulo 연산을 곱셈과 시프트 연산으로 대체한다.
  • 자세한 내용을 알고 싶다면, 이 위키피디아 글과 이 min_25의 글을 확인하라.
  • 어셈블리를 잘 아는 사람이라면 어셈블리를 이용할 수 있는 것으로 안다. 이 부분은 내가 모르니 생략. 이 링크를 보자.
  • 관련 내용이 KAIST의 ICPC팀이었던 더불어민규당의 팀노트에도 있다. Fast LL Division/Modulo 부분을 보자.


 

'PS > PS 정수론 가이드' 카테고리의 다른 글

5. 팩토리얼과 이항계수  (6) 2020.12.26
4. 페르마 소정리, 오일러 정리 및 활용  (2) 2020.12.24
3. 중국인의 나머지 정리  (0) 2020.12.24
2. 유클리드, 확장 유클리드  (3) 2020.12.24
0. Introduction  (11) 2020.12.24

PS를 위한 정수론 가이드를 쓰기로 했다. 이유는 여러 가지.

  • 자료가 부족하다는 의견을 많이 듣고, 동의함
  • kipa00님의 NTA가 매우 좋은 자료인 것은 맞지만, 진입장벽이 상당하다
  • 수학적 직관이나 엄밀한 증명을 일부 포기하더라도 내용을 간결하게 전달하는 것이 필요한 것 같다
  • (나중에 직관/증명이 필요하다면, 그 때 직접 자료를 찾으면 된다. 처음 들이박는 것보다 쉬울 것)
  • 그래서 최대한 문제 풀이에 집중하는 가이드가 필요하다고 생각했다
  • 그러나, 각 알고리즘의 기본 원리 정도는 배우고 갈 수 있도록 내용을 작성하였다.
  • (목표는 PS 정수론을 입문하기 위해 책을 폈다가 수학적 귀납법부터 시작하면서 의지를 잃는 사람들을 배려하는 것) 

이론 설명은 여기에 간단하게 하고, 관련 코드를 github에 올릴 계획. git 연습도 해야하니. 

연습문제도 올릴 예정인데, 이건 천천히 할 생각이다. 초반 주제들은 solved.ac에 관련 태그가 많으니 활용하자.


글을 읽다보면 뭔가 논리에 빈 공간이 있다는 생각이 들 수도 있다. 정수론 책을 그대로 옮기고 싶지 않아서 그런 것이다.

논리의 허점에 아쉬움을 느끼는 때가 오면, 학부용 정수론 책을 사서 읽으면 도움이 될 것이다. 위에서도 말했지만..

(+ 당연한거지만 학부 정수론을 모르는 상태로 어려운 문제를 풀려고 하면 한계가 올 것이다. 그때 필요하면 책을 읽으면 된다. 앞 몇 장만 읽어도 도움된다.)


일단 내용을 최대한 빠르게 퀄리티 신경 안 쓰면서 뽑아내고, 지적이나 제안을 받아가면서 천천히 보완할 생각이다. 

한 번에 완벽한 자료를 만들려고 하면 한 3~4단원하고 접을 가능성이 99.9%라서인데 사실 이게 더 나을수도 ㅋㅋㅋㅋ

가끔 오개념을 글에 쓰고 난 다음 나중에 고치는 일이 생길 수 있어, 글이 올라온 뒤 조금 시간이 지난 다음 읽는 것을 추천한다.


추가적으로 해야하는 언급들

  • 시간복잡도 분석은 빡빡하게 하지 않을 것이다. 본격적으로 시간복잡도를 논의하려면 정수 덧셈도 로그 시간이라는 내용들을 녹여내야한다. 하지만 PS/CP에서 이렇게 시간복잡도를 분석하는 경우는 흔치 않으니, 여기서도 모든 덧셈과 곱셈은 $\mathcal{O}(1)$ 연산이라는 것으로 합의하자. 
  • 비슷하게, $\mathcal{O}$ notation도 꽤 편안하게 (엄밀하지 않게) 사용할 것이다. 적당히 이해하고 넘어가자 :)
  • 자료 수정/추가는 가끔 오류가 보이거나 필요가 생기면 할 것이다. github에서도 수정이 많을 수 있다.
  • github에 올라온 코드는 팀노트에 복붙해서 넣을 퀄리티가 아니라, proof of concept 정도로 보는 것이 나을 것이다.


solved.ac 기준. 1~3 및 5 초반은 플레 중하위권 이하, 4, 5 후반은 플레 중상위권, 6번은 플레, 7, 8은 플레 상위권, 9~11번은 다이아 이상.


예상 커리큘럼

  1. 기본기 잡기
  2. 유클리드, 확장 유클리드 알고리즘
  3. 중국인의 나머지 정리
  4. 페르마 소정리, 오일러 정리 및 활용
  5. 팩토리얼과 이항계수
  6. Miller-Rabin 소수 판별 알고리즘과 Pollard-Rho 소인수분해
  7. Mobius function과 그 활용
  8. 원시근, 이산로그, 이산제곱근
  9. 유클리드 알고리즘의 활용
  10. 소수의 개수 빠르게 세기
  11. more on multiplicative functions
  12. 개인적인 후기, 그리고 그 이후의 이야기


1단원은 이미 자료가 많은 부분을 다루어, 덜 친절할 예정이다. 이 부분은 다른 자료를 찾는 것을 추천한다.


UPD: 백준 문제집 - 6593번 ~ 6603번


질문은 댓글로 항상 환영 :) (나도 내가 설명을 쉽게 못하는 사람인 걸 안다)

'PS > PS 정수론 가이드' 카테고리의 다른 글

5. 팩토리얼과 이항계수  (6) 2020.12.26
4. 페르마 소정리, 오일러 정리 및 활용  (2) 2020.12.24
3. 중국인의 나머지 정리  (0) 2020.12.24
2. 유클리드, 확장 유클리드  (3) 2020.12.24
1. 기본기 잡기  (14) 2020.12.24



그래도 후기를 쓰기는 해야 할 것 같아서 ㅋㅋㅋㅋㅋ 아쉽지만 작년에 비해서 좋은 결과가 나오지는 못했다. 이유는

  • 작년처럼 팀연습을 10번 이상 열심히 굴리지를 않았음 (올해 팀연습 0회)
  • 팀들이 작년보다 전반적으로 강해지고 센 팀들도 더 등장함
  • 일단 내가 작년에 비해서 간절함이 없었음 (애초에 작년에 뽑은 성과가 너무 좋았음) 

그래서 솔직히 결과에 아쉬움이 있어도 "이거 이상을 바라면 그건 그거대로 양심 X"라고 생각하고 있다.

아쉽다고 쳐도 9등이면 동상이고 이정도면 꽤 깔끔한 CP 대회 마무리 아닐까 싶기도 하고 ^__^ 

  • 열었더니 B가 쉬워서 빠르게 풀었음
  • A가 풀이 자체는 쉽다는 콜이 나옴. C 문제 해석에서 약간의 뇌절
  • E가 풀만하다는 콜과 G도 쉽다는 콜이 나옴. G에서 함정 빠짐
  • E 맞추고 G는 계속 맞왜틀. J 풀이 나옴. H가 simple FFT인거 보고 바로 구현, AC
  • C도 풀이 나오고 L 준비 시작. J 짜서 AC. G 다른 풀이도 짜봤는데 또 틀림.
  • G 함정 발견 및 C 구현 후 C, G 모두 AC. L은 DnC 콜이 나왔는데 틀리는 중.
  • L DM 돌리면서 틀린 이유 발견. Money For Nothing 생각하면서 풀이 고쳐서 맞음
  • 이제 junie가 A 구현하면서 다른 풀 거 찾아 떠남. I 보는게 맞는 판단인데 F가 할만해보여서 (ㅋㅋ) 시작
  • F 문제 잘못 이해해서 SCC 문제인줄 알았음 ㅋㅋ
  • junie가 A를 개고통받으면서 결국 AC 성공. 8솔해서 대충 이정도면 그래도 나쁘지 않다고 생각했음
  • F 예제 만들어보다가 결국 문제 이해를 못해먹겠다는 결론을 얻고 그대로 사망

이제 내년부터 ICPC 판 재밌어질텐데 팝콘각이나 열심히 재야겟다 ^____^

'PS > 대회 후기' 카테고리의 다른 글

SCPC 2021 2차 예선 풀이  (1) 2021.08.07
SCPC 2021 1차 예선 풀이  (0) 2021.07.16
SCPC 2020 본선 후기  (5) 2020.11.09
ACM-ICPC Seoul Regional Preliminary 2020  (0) 2020.10.19
SCPC 2020 2차 예선 풀이  (1) 2020.09.05




2등상을 받았다. 실제 실력에 비해 너무 높은 상이다.


UPD: 상금과 트로피까지 전달 받았다. 이제 수상 소감이 올라오면 끝인데 얼굴 팔릴 생각에 걱정이다 ㅜㅜ

UPD: 수상 소감까지 올라왔습니다. 얼굴도 팔렸네요 ㅋㅋ;


대회 과정

  • 1번 열었더니 쉬워서 빠르게 풀었다. 새로고침하니 퍼솔 같아서 기분이 좋았음 ^~^
  • 2번 열고 일단 SCC를 짤 줄 알면 $\mathcal{O}(m^2)$이 뚝딱임을 파악했다
  • 그런데 뇌절해서 그 풀이가 $\mathcal{O}(nm)$인 것으로 착각을 했다.
  • 아무튼 $\mathcal{O}(m^2)$ 풀이를 짜서 긁으려고 했는데 틀리더라. 멘붕 옴
  • 나중에 내 풀이가 $\mathcal{O}(nm)$이 아님을 깨달았다. 그래서 목표를 $\mathcal{O}(nm)$ 풀이 찾는 거로 잡았다.
  • 그러다가 문제에 오류가 있다는 공지를 받고, $\mathcal{O}(nm)$ 풀이 찾기에 착수했다.
  • 풀이 자체는 빠르게 나왔는데, 2번 문제치고 너무 복잡해서 뇌절이었나 걱정했다.
  • 그런데 2번 데이터가 수정된 이후에도 만점자가 4명 뿐이더라. 시간이 꽤 지난 것 치고 적은 수였다.
  • 그래서 맞는 풀이라는 확신을 할 수 있었고. 빠르게 짜서 바로 AC. 5번째 솔브로 기억한다. 이때가 1시간 정도 지났을 때.
  • 3번을 열었는데 일단 문자열에다가 드럽게 복잡한 것 같아서 일단 걸렀다. 전날에 연습을 좀 했는데 힘겨워서 ㅋㅋ
  • 그래도 3번은 풀어야 한다고 생각을 했고, 그 전에 4, 5번에서 빠르게 긁을 수 있는 것을 긁어오는 게 좋다고 판단했다.
  • 4번을 열었는데, 일단 마음 속으로 $\mathcal{O}(n^3 \log n \log MAX)$ 쯤 되는 풀이가 나왔다. 4-2까지 긁힐 것으로 예상했다.
  • 짜는 것도 별로 어렵지 않아서 빠르게 짰는데, 예제 1이 안나오네? 사실 상당히 큰 가정을 기반으로 한 풀이였다.
  • 예제가 안 나온다는 것은 가정이 틀렸다는 거고, 결국 간단하게 고치는 것 자체가 쉽지 않다는 것이다. 똑떨...
  • 5번을 열었더니 뭔 이상한 트리가 튀어나와서 바로 걸렀다. 결국 소득 없이 3번으로 돌아왔다.
  • 3번의 풀이 자체는 스트링 티피컬 짬뽕 국밥 든든 그 자체여서 빠르게 찾을 수 있었다. 문자열 관련 대비도 꽤 했고.
  • 그런데 아무리 생각해도 코드가 300줄 뽑히게 생겨서 마음을 굳게 먹어야 했다. 다행히 이때도 2번 만점자 수가 20명인가 그랬다.
  • 3번을 풀면 매우 큰 상을 받을 가능성이 높다고 생각해서, 뇌절하지 말자고 생각하고 짜기 시작했다
  • 대충 한 파트 짜고 -> 스코어보드 확인하면서 안도하고 -> 한 파트 짜고 -> 안도하고를 반복
  • 다 짰더니 에러가 조금 났는데, 인덱스 실수 하나여서 금방 고쳤다. 예제 나와서 제출했더니 한 방에 맞더라 ㅋㅋ
  • 이때가 4시 15분. (3시간 15분 지났을 때) 이제 긁을 것을 찾아서 떠났고 5-1이 긁기 쉽다는 결론을 얻었다.
  • 사실 긁기 별로 안 어려운데, 뇌가 극도의 흥분상태여서 뇌절도 꽤 해서 40분 정도 걸렸다. 이때가 5시.
  • 4번을 긁으면 진짜 2등상에 못 박는 느낌이어서 무리를 좀 했다. 뚫으려고 별 짓을 다하다가 사망하고 패배를 인정했다. 


문제 풀이


1. 더도 말고 덜도 말고


매우 쉬운 문제. 백준에도 비슷한 문제가 많을 것으로 생각한다. std::map 사용하면 뚝딱.


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
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
mt19937 rng(4672627);
 
ll n, k, ans;
map<ll, ll> M;
 
void solve(void)
{
    ans=0; ll i, tot=0, x; cin>>n>>k;
    M.clear(); M[0]++;
    for(i=1 ; i<=n ; i++)
    {
        cin>>x; tot+=x;
        ans+=M[tot-k];
        M[tot]++;
    }
    cout<<ans<<"\n";
}
 
int main(void)
{
    fio; int i, tc;
    // freopen("input.txt", "r", stdin);
    // freopen("output.txt", "w", stdout);
    cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); // reset here
    }
    return 0;
}
cs


2. 영원한 휴가


작년 2번에 비해서 상당히 난이도가 올라간 문제. solved.ac 기준 플레 1 ~ 다이아 5 정도라고 개인적으로 생각한다.

$\mathcal{O}(m^2)$ 풀이는 간단한데, 그냥 SCC를 모든 경우에 대해서 다 구해주면 된다. 이러면 섭테 2까지 긁힌다.

본 문제를 해결하려면 SCC를 압축하여 DAG를 만드는 과정을 생각할 필요가 있다. 이제 기존 간선 $u \rightarrow v$를 뒤집었다고 생각하자.

  • $u, v$가 같은 SCC에 포함되었다면, 그 간선을 뒤집어서 SCC 크기가 증가하지는 않을 것이다. 그러니 무시해도 된다.
  • $u, v$가 다른 SCC에 포함되었다면, DAG 상에서 $u$의 SCC에서 $v$의 SCC로 가는 간선이 있다는 것이다.
  • 만약 이 간선을 뒤집는다면, 어떻게 될까? 만약 해당 간선이 $u$의 SCC에서 $v$의 SCC로 가는 유일한 경로라면, 무시해도 된다.
  • 하지만 $u$의 SCC에서 $v$의 SCC로 가는 경로가 2개 이상이라면, $u$의 SCC와 $v$의 SCC "사이"에 있는 모든 SCC가 묶인다.
  • 여기서 "사이"에 있다는 것은, $u$의 SCC -> ??? -> $v$의 SCC 형태의 경로가 존재하는 ??? 들을 의미한다.

이제 문제를 해결할 수 있다. SCC 압축 DAG를 만든 후, $dp[u][v]$를 $u$번 SCC에서 $v$번 SCC로 가는 경로의 개수가 0개인지, 1개인지, 아니면 2개 이상인지로 정의하자. 이 값은 DAG에서 위상정렬 DP를 하듯이 계산할 수 있으며, 시간복잡도는 $\mathcal{O}(nm)$이다. 이 값을 잘 계산해놓으면 위 관찰을 통해서 답을 얻는 것 역시 쉽게 할 수 있다. 구현을 효율적으로 해야 AC를 얻는다는 말이 있으니, 잘 구현해야한다. $dp$ 계산에서 오버플로우에 주의하자.


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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
mt19937 rng(4672627);
 
int n, m, cnt, gr, org;
int ord[1111];
vector<int> cedge[1111];
vector<int> redge[1111];
int vis[1111], whi[1111], sz[1111];
pair<intint> E[11111];
int forw[1111][1111];
int cntw[1111][1111];
int indeg[1111];
queue<int> Q;
 
void dfs1(int v)
{
    vis[v]=1;
    for(int i=0 ; i<cedge[v].size() ; i++)
        if(!vis[cedge[v][i]]) dfs1(cedge[v][i]);
    ord[++cnt]=v;
}
 
void dfs2(int v)
{
    vis[v]=1; whi[v]=gr; sz[gr]++;
    for(int i=0 ; i<redge[v].size() ; i++)
        if(!vis[redge[v][i]]) dfs2(redge[v][i]);
}
 
int calc(void)
{
    int u, v, i, j, ret=0; gr=0; cnt=0;
    // can be optimized
    for(i=1 ; i<=n ; i++) ord[i]=0;
    for(i=1 ; i<=n ; i++) cedge[i].clear();
    for(i=1 ; i<=n ; i++) redge[i].clear();
    for(i=1 ; i<=m ; i++)
    {
        u=E[i].first; v=E[i].second;
        cedge[u].push_back(v);
        redge[v].push_back(u);
    }
    memset(whi, 0sizeof(whi));
    memset(sz, 0sizeof(sz));
    memset(vis, 0sizeof(vis));
    for(i=1 ; i<=n ; i++)
        if(!vis[i]) dfs1(i);
    reverse(ord+1, ord+n+1);
    memset(vis, 0sizeof(vis));
    for(i=1 ; i<=n ; i++)
    {
        v=ord[i];
        if(!vis[v])
        {
            gr++; dfs2(v);
            ret=max(ret, sz[gr]);
        }
    }
    return ret;
}
 
void solve(void)
{
    int i, j, u, v; ll ans=0cin>>n>>m;
    for(i=1 ; i<=m ; i++)
        cin>>E[i].first>>E[i].second;
    org=calc();
   // for(i=1 ; i<=n ; i++) cout<<whi[i]<<" "; cout<<"\n";
   // for(i=1 ; i<=gr ; i++) cout<<sz[i]<<" "; cout<<"\n";
    memset(indeg, 0sizeof(indeg));
    for(i=1 ; i<=gr ; i++)
        for(j=1 ; j<=gr ; j++) cntw[i][j]=0, forw[i][j]=0;
    for(i=1 ; i<=n ; i++) cedge[i].clear();
    for(i=1 ; i<=n ; i++) redge[i].clear();
    for(i=1 ; i<=m ; i++)
    {
        u=E[i].first; v=E[i].second;
        if(whi[u]!=whi[v])
        {
            cntw[whi[u]][whi[v]]++;
            cedge[whi[u]].push_back(whi[v]);
            indeg[whi[v]]++;
        }
    }
    while(!Q.empty()) Q.pop();
    for(i=1 ; i<=gr ; i++) forw[i][i]=1;
    for(i=1 ; i<=gr ; i++if(!indeg[i]) { Q.push(i); forw[i][i]=1; }
    while(!Q.empty())
    {
        int v=Q.front(); Q.pop();
        for(i=0 ; i<cedge[v].size() ; i++)
        {
            int nxt=cedge[v][i];
            int cntv=cntw[v][nxt];
            for(j=1 ; j<=gr ; j++)
            {
                if(forw[j][v]==0continue;
                if(forw[j][v]==1 && cntv==1) forw[j][nxt]++;
                if(forw[j][v]>=2 || cntv>=2) forw[j][nxt]+=2;
            }
            indeg[nxt]--if(indeg[nxt]==0) { forw[nxt][nxt]=1; Q.push(nxt); }
        }
    }
    for(i=1 ; i<=m ; i++)
    {
        u=E[i].first; v=E[i].second;
        if(whi[u]!=whi[v] && forw[whi[u]][whi[v]]>=2)
        {
            int tot=0;
            for(j=1 ; j<=gr ; j++)
            {
                if(forw[whi[u]][j]>=1 && forw[j][whi[v]]>=1)
                    tot+=sz[j];
            }
            if(tot>org) ans+=tot;
        }
    }
    cout<<ans<<"\n";
    for(i=1 ; i<=n ; i++) cedge[i].clear();
    for(i=1 ; i<=n ; i++) redge[i].clear();
    for(i=1 ; i<=n ; i++) whi[i]=sz[i]=0; gr=org=ans=0;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); // reset here
    }
    return 0;
}
cs


3. 회문인 부분 문자열


역시 작년에 비해 매우 어렵다. 다3 정도라고 생각. 복잡한 문제인 만큼 천천히 생각해보자. 해야 하는 것은

  • 우선 회문인 부분 문자열을 전부 뽑아낸다.
  • 그 중 조건을 만족하는 부분 문자열을 전부 뽑아낸다. (즉, 주어진 회문이 어떤 문자열에 포함되는지 판별해야 한다)
  • 그 후, 그 부분 문자열들을 사전순으로 정렬해서 $k$번째 것이 무엇인지를 파악해야 한다.

이제 각 단계를 어떻게 할 수 있을지 생각해보자.

  • APIO 2014 팰린드롬의 풀이를 생각하자. 길이 $n$ 문자열의 회문인 서로 다른 부분 문자열의 개수는 최대 $n$개다.
  • 이 시점에서 $k$의 제한은 페이크임을 알 수 있다. 또한, 회문을 모두 뽑는 것은 Manacher 알고리즘으로 가능하다.
  • 서로 다른 것을 뽑아냈는지를 확인해야 하는데, 이건 Rabin-Karp 해시로 가능하다. 충돌 안하게 모듈로 2개 적당히 잡자.
  • 위 koosaga 블로그 링크의 설명을 보면 사실 이 부분을 이분탐색 없이 할 수 있는데, 당시에는 이분탐색까지 덮었다.

이러면 1단계가 끝난다. 여기까지 하고 한 번 예제 넣어서 잘 돌아가는지 확인했다. 이제 2단계.

  • 어쨌든 후보들은 "특정 문자열의 부분문자열인 회문"들이다. 이제 이것들이 다른 문자열에도 포함되는지 확인해야 한다.
  • 그러면 세 문자열을 합친 (사이에 이상한 문자 껴넣고) 새 문자열을 만든 후, Suffix Array + LCP + Segment Tree를 적용한다.
  • 이 과정 자체는 꽤 티피컬한데 구현이 드럽게 힘들고 귀찮다. Suffix Array는 로그제곱으로 짜도 충분하다. 

이러면 2단계가 끝난다. 여기까지 하고 한 번 예제 넣어서 잘 돌아가는지 확인했다. 이제 마지막 단계.

  • 이제 부분문자열을 사전순 정렬해야 하는데, 단순하게 Suffix Array 상으로 정렬하면 안된다. (why?)
  • 제대로 정렬하려면 "이 회문을 prefix로 갖는 suffix 중 가장 Suffix Array에서 앞에 있는 것"을 찾을 필요가 있다.
  • 이를 위해서는 또 한 번 LCP + Segment Tree + 이분탐색을 적용하면 된다. 겁나 귀찮은데 해야함
  • 이제 회문들을 "SA 위치, 회문의 길이"라는 pair로 정렬하면 사전순 정렬이 됨을 확인할 수 있다. 끝.

가치가 있다고 생각해서, 디버깅 용 코드까지 전부 그대로 올린다. 


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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
mt19937 rng(4672627);
const ll mod1=1561035197;
const ll mod2=1871313779;
const ll p1=27386137;
const ll p2=73186173;
 

int tree[333333], C=160000;
int k, L[3], d, LEN, FINCOUNT;
string s[3], S;
int len_odd[3][55555];
int len_tr[3][111111];
int len_ev[3][55555];
ll hsh1[3][55555];
ll hsh2[3][55555];
ll pt1[55555];
ll pt2[55555];
int LCP[155555];
int SA[155555];
int rk[155555];
int pos[155555];
int tp[155555];
vector<int> POS[3];
vector< pair< pair<intint>int> > indx;
// which string, location, length
pair<intint> fin[155555];
// location in final string in (SA), length
set< pair<ll, ll> > EX; // already inserted hashes
 
bool cmp(const int U, const int V)
{
    if(pos[U]!=pos[V]) return pos[U]<pos[V];
    if(U+d<LEN && V+d<LEN) return pos[U+d]<pos[V+d];
    return U>V; // okay!
}
 
void calc_LCPSA(void)
{
    int i, j, c; LEN = S.length();
    for(i=0 ; i<LEN ; i++) pos[i]=S[i], SA[i]=i;
    for(d=1 ; d<=LEN ; d<<=1)
    {
        sort(SA, SA+LEN, cmp); tp[0]=0;
        for(i=1 ; i<LEN ; i++) tp[i]=tp[i-1]+cmp(SA[i-1], SA[i]);
        for(i=0 ; i<LEN ; i++) pos[SA[i]]=tp[i];
        if(tp[LEN-1]==LEN-1break;
    }
    for(i=0 ; i<LEN ; i++) rk[SA[i]]=i;
    for(i=0, j=0 ; i<LEN ; i++, j=max(j-10))
    {
        if(rk[i]==LEN-1continue; c=SA[rk[i]+1];
        for( ; i+j<LEN && c+j<LEN && S[i+j]==S[c+j] ; j++);
        LCP[rk[i]]=j;
    }
}
 
void update(int loc, int v)
{
    loc+=C; tree[loc]=v;
    for( ; loc>1 ; loc>>=1)
        tree[loc>>1]=min(tree[loc], tree[loc^1]);
}
 
int query(int l, int r)
{
    int ret=1e9;
    for(l+=C, r+=C ; l<r ; l>>=1, r>>=1)
    {
        if(l&1) ret=min(ret, tree[l++]);
        if(r&1) ret=min(ret, tree[--r]);
    }
    return ret;
}
 
pair<ll, ll> hasher(int idx, int l, int r)
{
    // [l, r] = [0, r] - [0, l-1] * p^(r-l+1)
    if(l==0return make_pair(hsh1[idx][r], hsh2[idx][r]);
    ll ret1=(hsh1[idx][r]-(hsh1[idx][l-1]*pt1[r-l+1]%mod1));
    if(ret1<0) ret1+=mod1;
    ll ret2=(hsh2[idx][r]-(hsh2[idx][l-1]*pt2[r-l+1])%mod2);
    if(ret2<0) ret2+=mod2;
    return make_pair(ret1, ret2);
}
 
void get_hash(int idx)
{
    ll cur1=0, cur2=0, i;
    for(i=0 ; i<s[idx].length() ; i++)
    {
        hsh1[idx][i]=(cur1*p1+(s[idx][i]-'a'+1))%mod1; cur1=hsh1[idx][i];
        hsh2[idx][i]=(cur2*p2+(s[idx][i]-'a'+1))%mod2; cur2=hsh2[idx][i];
    }
}
 
 
bool isok(int idx, int l, int r)
{
    pair<ll, ll> TT = hasher(idx, l, r);
    if(EX.count(TT)) return true;
    return false;
}
 
int getloc(int idx, int l)
{
    if(idx==0return l;
    if(idx==1return L[0]+1+l;
    if(idx==2return L[0]+L[1]+2+l;
}
 
bool isinside(int idx, int RKloc, int goal)
{
    int cc=lower_bound(POS[idx].begin(), POS[idx].end(), RKloc)-POS[idx].begin();
    // POS[cc], POS[cc-1] trial
    if(cc < POS[idx].size())
    {
        // RKloc ~ cc distance
        int rv=query(RKloc, POS[idx][cc]);
        if(rv>=goal) return true;
    }
    if(cc-1 >=0)
    {
        int rv=query(POS[idx][cc-1], RKloc);
        if(rv>=goal) return true;
    }
    return false;
}
 
int get_front(int RKloc, int goal)
{
    int lef=0int rig=RKloc-1int best=RKloc, mid;
    while(lef<=rig)
    {
        mid=(lef+rig)/2;
        if(query(mid, RKloc)>=goal) best=mid, rig=mid-1;
        else lef=mid+1;
    }
    return best;
}
 
void add_work(int idx, int l, int r)
{
   // cout<<idx<<" "<<l<<" "<<r<<endl;
    pair<ll, ll> TT = hasher(idx, l, r); EX.insert(TT);
    // one should contain, other should not
    // [l, r] at idx
    int mylen=r-l+1int myloc=rk[getloc(idx, l)];
    int others=0;
    if(isinside((idx+1)%3, myloc, mylen)) others++;
    if(isinside((idx+2)%3, myloc, mylen)) others++;
    int newmyloc=get_front(myloc, mylen);
    if(others==1)
    {
        FINCOUNT++;
        fin[FINCOUNT]=make_pair(newmyloc, mylen);
    }
}
 
void finwork(int idx)
{
    int i, j; // odd insert
    for(i=0 ; i<L[idx] ; i++)
    {
        int lef=1, rig=len_odd[idx][i], mid, best=0;
        while(lef<=rig)
        {
            mid=(lef+rig)/2;
            if(isok(idx, i-mid+1, i+mid-1)) best=mid, lef=mid+1;
            else rig=mid-1;
        }
        for(j=best+1 ; j<=len_odd[idx][i] ; j++)
            add_work(idx, i-j+1, i+j-1);
    }
    for(i=0 ; i<L[idx] ; i++)
    {
        if(len_ev[idx][i]==0continue;
        int lef=1, rig=len_ev[idx][i], mid, best=0;
        while(lef<=rig)
        {
            mid=(lef+rig)/2;
            if(isok(idx, i-mid+1, i+mid)) best=mid, lef=mid+1;
            else rig=mid-1;
        }
        for(j=best+1 ; j<=len_ev[idx][i] ; j++)
            add_work(idx, i-j+1, i+j);
    }
}
 
// will do [l, r] form
 
void run_manacher(int idx)
{
    int i, l=1, r=1; L[idx]=s[idx].length();
    string Ns = "@" + s[idx] + "#";
    for(i=1 ; i<=L[idx] ; i++)
    {
        len_odd[idx][i]=min(r-i, len_odd[idx][l+(r-i)]);
        while(Ns[i-len_odd[idx][i]] == Ns[i+len_odd[idx][i]]) len_odd[idx][i]++;
        if(i+len_odd[idx][i]>r)
        {
            l=i-len_odd[idx][i];
            r=i+len_odd[idx][i];
        }
    }
    for(i=0 ; i<L[idx] ; i++) len_odd[idx][i]=len_odd[idx][i+1];
    string NNs = "{";
    for(i=0 ; i<L[idx] ; i++)
    {
        NNs.push_back(s[idx][i]);
        if(i!=L[idx]-1) NNs.push_back('#');
    }
    NNs.push_back('}'); // {a#b#c}
    ll t=NNs.length()-2; l=r=1;
    for(i=1 ; i<=t ; i++)
    {
        len_tr[idx][i]=min(r-i, len_tr[idx][l+(r-i)]);
        while(NNs[i-len_tr[idx][i]] == NNs[i+len_tr[idx][i]]) len_tr[idx][i]++;
        if(i+len_tr[idx][i]>r)
        {
            l=i-len_tr[idx][i];
            r=i+len_tr[idx][i];
        }
    }
    for(i=2 ; i<=t ; i+=2)
    {
        int lc=i/2-1;
        len_ev[idx][lc]=len_tr[idx][i]/2;
    }
}
 
void solve(void)
{
    FINCOUNT=0;
    int i, j; EX.clear();
    for(i=0 ; i<3 ; i++) POS[i].clear();
    memset(tree, 0sizeof(tree)); indx.clear();
    cin>>s[0]>>s[1]>>s[2]>>k;
    memset(len_odd, 0sizeof(len_odd));
    memset(len_ev, 0sizeof(len_ev));
    memset(len_tr, 0sizeof(len_tr));
    for(i=0 ; i<3 ; i++) get_hash(i);
    for(i=0 ; i<3 ; i++) run_manacher(i);
    /* testing manacher
    for(i=0 ; i<3 ; i++)
    {
        for(j=0 ; j<L[i] ; j++) cout<<len_odd[i][j]<<" "; cout<<"\n";
        for(j=0 ; j<L[i] ; j++) cout<<len_ev[i][j]<<" "; cout<<"\n";
    }
    */
    S = s[0+ '}' + s[1+ '}' + s[2];
    calc_LCPSA(); LEN=S.length();
    for(i=0 ; i<=L[0]-1 ; i++) POS[0].push_back(rk[i]);
    for(i=L[0]+1 ; i<=L[0]+L[1] ; i++) POS[1].push_back(rk[i]);
    for(i=L[0]+L[1]+2 ; i<=L[0]+L[1]+L[2]+1 ; i++) POS[2].push_back(rk[i]);
    for(i=0 ; i<3 ; i++) sort(POS[i].begin(), POS[i].end());
    for(i=0 ; i<=LEN-2 ; i++) update(i, LCP[i]);
 
    /* LCP, SA check
    for(i=0 ; i<=LEN-1 ; i++)
    {
        cout<<S.substr(SA[i], S.length())<<"\n";
        cout<<LCP[i]<<"\n";
    }
    */
    //cout<<"FINWORKSTART"<<"\n"; return;
    for(i=0 ; i<3 ; i++) finwork(i);
    sort(fin+1, fin+FINCOUNT+1);
    if(FINCOUNT<k) { cout<<-1<<endlreturn; }
    int tt1=fin[k].first;
    int tt2=fin[k].second;
    for(i=0 ; i<tt2 ; i++cout<<S[SA[tt1]+i];
    cout<<endlreturn;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    pt1[0]=1; pt2[0]=1;
    for(i=1 ; i<=55550 ; i++) pt1[i]=(p1*pt1[i-1])%mod1;
    for(i=1 ; i<=55550 ; i++) pt2[i]=(p2*pt2[i-1])%mod2;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); // reset here
    }
    return 0;
}
cs


5. 돌 옮기기


5-1을 긁는 것은 어렵지 않다. 상태 전이를 전부 파악하고 union-find를 돌리면 된다.

상태 전이를 효율적으로 하는 게 약간 헷갈릴 수 있는데, 비트마스킹을 잘 활용하면 $\mathcal{O}(n \cdot 2^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
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
mt19937 rng(4672627);
 
int n, kt, msk1, msk2;
vector<int> edge[22];
int con[21][21];
int whi1[211111];
int whi2[211111];
int unionmask[1<<20];
int frst[1<<20];
int mymask[21];
int par[1<<20];
int actok[1<<20];
 
int find(int x)
{
    if(par[x]==x) return x;
    return par[x]=find(par[x]);
}
 
void merge(int u, int v)
{
    if(!actok[u] || !actok[v]) return;
    u=find(u); v=find(v);
    if(u==v) return;
    par[u]=v; return;
}
 
void solve(void)
{
    bool actualwork=false;
    cin>>n; int i, j, k, u, v;
    memset(con, 0sizeof(con));
    if(n<=20) { actualwork=true; }
    for(i=1 ; i<=n-1 ; i++)
    {
        cin>>u>>v; u--; v--;
        if(actualwork)
        {
            con[u][v]=1; con[v][u]=1;
            edge[u].push_back(v);
            edge[v].push_back(u);
        }
    }
    cin>>kt;
    for(i=1 ; i<=kt ; i++cin>>whi1[i]>>whi2[i];
    for(i=1 ; i<=kt ; i++) whi1[i]--, whi2[i]--;
    if(!actualwork)
    {
        cout<<0<<endl;
        return;
    }
    memset(mymask, 0sizeof(mymask));
    memset(unionmask, 0sizeof(unionmask));
    memset(actok, 0sizeof(actok)); actok[0]=1;
    for(i=0 ; i<n ; i++)
    {
        for(j=0 ; j<n ; j++)
        {
            if(con[i][j]) mymask[i]|=(1<<j);
        }
    }
    for(i=0 ; i<n ; i++) mymask[i]|=(1<<i);
    for(i=1 ; i<(1<<n) ; i++)
    {
        int cc=frst[i];
        unionmask[i]=unionmask[i^(1<<cc)]|mymask[cc];
    }
    for(i=1 ; i<(1<<n) ; i++)
    {
        int cc=frst[i];
        if(!actok[i^(1<<cc)]) continue;
        if((1<<cc)&(unionmask[i^(1<<cc)])) continue;
        actok[i]=1;
    }
    for(i=0 ; i<(1<<n) ; i++) par[i]=i;
    for(i=0 ; i<(1<<n) ; i++)
    {
        if(!actok[i]) continue;
        for(j=0 ; j<n ; j++)
        {
            if(!(i&(1<<j))) continue;
            int remmsk=i^(1<<j);
            for(k=0 ; k<edge[j].size() ; k++)
            {
                if(remmsk&(1<<edge[j][k])) continue;
                if(!(unionmask[remmsk]&(1<<edge[j][k])))
                    merge(i, remmsk|(1<<edge[j][k]));
            }
        }
    }
    int ans=0int cur1=0int cur2=0;
    for(i=1 ; i<=kt ; i++)
    {
        cur1|=(1<<whi1[i]);
        cur2|=(1<<whi2[i]);
        if(find(cur1)==find(cur2)) ans++;
    }
    for(i=0 ; i<n ; i++) edge[i].clear();
    cout<<ans<<endl;
}
 
 
int main(void)
{
    fio; int i, j, tc; cin>>tc;
    frst[0]=0; frst[1]=0;
    for(i=1 ; i<20 ; i++)
        for(j=1<<i ; j<(1<<(i+1)) ; j++) frst[j]=i;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); // reset here
    }
    return 0;
}
cs


'PS > 대회 후기' 카테고리의 다른 글

SCPC 2021 1차 예선 풀이  (0) 2021.07.16
ACM-ICPC Seoul Regional 2020  (0) 2020.11.20
ACM-ICPC Seoul Regional Preliminary 2020  (0) 2020.10.19
SCPC 2020 2차 예선 풀이  (1) 2020.09.05
SCPC 2020 1차 예선 풀이  (0) 2020.08.22


매우 늦게 올린다. 결과는 6등으로, 예상보다는 매우 높았으나 여전히 만족스러운 결과는 아니었다. J를 풀지 못한것이 아쉬움.

작년의 팀과 멤버는 동일하다. rkm0959, junie, bjwj5505다. 내가 1년간 공부를 많이 한 건 맞지만, 정작 최근에는 CTF 참가 등으로 PS, CP에 집중하지 못해서 팀의 전체적인 실력이 강화된건지 퇴화된건지 좀 애매한 것 같다. 다른 팀원도 비슷하다. 일단 지금까지는 나쁘지 않게 하고 있는듯.

 

타임라인과 함께 간략한 설명을 붙이도록 하겠다. 모든 사람을 3인칭으로 명명하겠다.

  • 시작하자마자 문제 12개 모두 출력
  • 스코어보드 보니까 I가 0분컷으로 풀렸네? 진짜 쉽네?
  • rkm0959가 바로 컴퓨터 잡고 AC (5분)
  • 이때쯤 E도 퍼솔이 나왔고, 바로 rkm0959가 이어서 AC (8분)
  • bjwj5505는 이때 L을 읽고 있었고, junie는 앞쪽 문제를 읽는 중.
  • bjwj5505가 L을 구현했지만 WA 후 코드 프린트
  • rkm0959는 G를 보면서 수학 문제라고 생각을 했으나 매우 어려워보여서 대기중
  • 대신 K가 쉽다는 것을 확인하고, 풀이를 bjwj5505와 확인
  • rkm0959가 H의 풀이를 찾고, 정당성을 bjwj5505와 확인 
  • 그러다가 F가 쉬워보여서 rkm0959, junie가 해결 시도
  • bjwj5505가 L을 디버깅하고 있으며, rkm0959가 K를 짜기 시작
  • bjwj5505가 L을 고치고 AC (39분), rkm0959도 곧 K AC (46분)
  • bjwj5505, junie가 F의 풀이를 찾는동안 rkm0959 H 구현
  • F의 풀이를 찾고 AC (51분), H도 곧 AC (58분) - 1시간 6솔브 도달
  • 정확하게 기억은 안나지만 junie가 느린 J 풀이를 찾았음 (로그 2개)
  • 그러나 정확하게 풀이를 이해하고 있는 사람은 junie 혼자
  • bjwj5505, junie는 A의 해결에 집중. rkm0959는 B가 DP임을 확인
  • rkm0959가 D가 세그먼트 트리로 해결될 수 있는 문제임을 확인
  • A, B, D의 동시다발적 구현 + WA + 디버깅 파티 후 전부 AC (115, 133, 149분)
  • J를 고치려고 했고, "맞는" 풀이를 1분 전에 찾았으나 TLE. 똑떨....


'PS > 대회 후기' 카테고리의 다른 글

ACM-ICPC Seoul Regional 2020  (0) 2020.11.20
SCPC 2020 본선 후기  (5) 2020.11.09
SCPC 2020 2차 예선 풀이  (1) 2020.09.05
SCPC 2020 1차 예선 풀이  (0) 2020.08.22
APIO 2020 Open Contest  (0) 2020.08.22


1. 실력 맞추기


기본적으로 최적의 방식는 각 그룹을 실력 순으로 나열한 뒤 매칭시키는 것이다.

이제 $A$ 그룹의 사람 한 명의 실력을 바꾼다고 가정하자. 이때, 새로운 실력이 $B$ 그룹 중 한 사람의 실력과 일치하는 경우만 고려해도 무방하다.

이제 $A_i$를 $B_j$로 바꾼다고 가정하자. 만약 $i=j$라면, 공정도의 합은 $\sum_{k=1, k \neq i}^{n} |A_k - B_k|$다. 

만약 $i < j$라면, 공정도의 합은 $\sum_{k=1}^{i-1} |A_k - B_k| + \sum_{k=i+1}^j |A_k - B_{k-1}| + \sum_{k=j+1}^n |A_k-B_k|$가 된다.

만약 $i > j$라면, 공정도의 합은 $\sum_{k=1}^{j-1} |A_k - B_k| + \sum_{k=j+1}^i |A_{k-1} - B_k| + \sum_{k=i+1}^n |A_k-B_k|$이다.

이제 각 $i, j$에 대하여 저 값들 중 최솟값을 구하면 된다. 그런데 식의 형태가 상당히 부분합 느낌이 강하게 난다.


$i=j$인 경우는 $|A_k - B_k|$에 대한 부분합을 전부 계산하는 방식으로 처리할 수 있다. 

$i < j$인 경우에는 $-|A_i-B_i| + \sum_{k=i+1}^j (|A_k - B_{k-1}| - |A_k - B_k|) $의 최솟값을 구하는 것과 같다.

이는 최대 연속 구간합을 계산하는 방식과 동일하게 계산할 수 있다. $i$를 고정하고 최적의 $j$를 쉽게 구할 수 있기 때문이다.

$i >j$인 경우에도 동일한 접근이 가능하며, 쉽게 계산하려면 $A$ 배열과 $B$ 배열을 swap 한 뒤 위 경우를 그대로 적용해도 된다.


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
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef unsigned long long int ull;
typedef long double ldb;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
ll n, ans, dfdf;
ll a[222222];
ll b[222222];
ll c[222222];
ll d[222222];
 
void work(void)
{
    ll i; d[n]=1e18;
    for(i=1 ; i<=n-1 ; i++) c[i]=c[i-1]+abs(a[i+1]-b[i])-abs(a[i+1]-b[i+1]);
    for(i=n-1 ; i>=1 ; i--) d[i]=min(d[i+1], c[i]);
    for(i=1 ; i<=n ; i++) ans=min(ans, dfdf+d[i]-c[i-1]-abs(a[i]-b[i]));
}
 
void solve(void)
{
    ll i, j, mx=0cin>>n; ans=0;
    for(i=1 ; i<=n ; i++cin>>a[i]; sort(a+1, a+n+1);
    for(i=1 ; i<=n ; i++cin>>b[i]; sort(b+1, b+n+1);
    for(i=1 ; i<=n ; i++) ans+=abs(a[i]-b[i]); dfdf=ans;
    for(i=1 ; i<=n ; i++) mx=max(mx, abs(a[i]-b[i])); ans-=mx;
    work(); for(i=1 ; i<=n ; i++) swap(a[i], b[i]); work();
    cout<<ans; return;    
}
 
int main(void)
{
    fio; ll i, tc; cin>>tc;
    for(i=1 ; i<=tc ;  i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


2. 고구마


$a_i + a_{i+1} + \cdots + a_j \le M$을 만족하면서, $a_i + a_{i+1} + \cdots + a_j$를 최대화하고 싶다.

부분합 배열 $ps_i = a_1 +a_2 + \cdots + a_i$를 만들면, 목표는 $ps_j - ps_{i-1}$을 조건 아래에서 최대화하는 것이다.

$j$를 고정하자. 그러면 목표는 $ps_{i-1} \ge ps_j - M$이면서 최소인 $ps_{i-1}$을 찾는 것이다.


이는 $ps$ 배열을 원소로 갖는 std::set을 이용하여 쉽게 찾을 수 있다. 1번보다 훨씬 쉬운 문제.


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
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef unsigned long long int ull;
typedef long double ldb;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
ll a[333333], ps[322222], n, M, ans;
set<ll> S; set<ll>::iterator it;
 
void solve(void)
{
    ll i; cin>>n>>M; S.clear(); ans=0;
    for(i=1 ; i<=n ; i++cin>>a[i]; S.insert(0);
    for(i=1 ; i<=n ; i++)
    {
        ps[i]=ps[i-1]+a[i];
        it=S.lower_bound(ps[i]-M);
        if(it!=S.end()) ans=max(ans, ps[i]-(*it));
        S.insert(ps[i]);
    }
    cout<<ans; return;
}
 
int main(void)
{
    fio; ll i, tc; cin>>tc;
    for(i=1 ; i<=tc ;  i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


3. 아르바이트


1차 4번 문제의 느낌이 나는 문제인 것 같다. 

우선 중요한 점은, 하루의 급여가 바뀐 경우 최대 $k$개의 구간의 총합이 변화한다는 점이다. 즉, 구간의 합이 변화하는 총 횟수는 최대 $Qk$번이다. 


$n-k+1$개의 구간의 총합을 관리하고 있는 multiset이 있다고 하자. 우리는 이 multiset의 중앙값을 계속 구해야 한다.

총합 자체를 관리하는 것은 $\mathcal{O}(Qk + n)$에 쉽게 할 수 있으므로, 우리의 궁극적인 문제는 다음과 같다.

  • 집합에 속한 원소 삭제
  • 집합에 원소 하나 추가
  • 집합의 중앙값 계산

다양한 접근이 가능하다. 첫 번째 접근은 세그먼트 트리를 이용한다.

가능한 구간의 합 $\mathcal{O}(Qk+n)$개를 전부 전처리 한 후 좌표압축하자. 

각 원소의 등장 횟수를 관리하는 합 세그먼트 트리를 관리하면, 이분탐색으로 답을 구할 수 있다.


세그먼트 트리를 구현할 경우 트리를 따라 내려가는 방식으로 이분탐색을 없앨 수 있다.

나는 중앙값을 계산하는 횟수가 $Q$로 적으므로, 세그먼트 트리 대신 펜윅을 사용하는 방식을 택했다.

그런데 이 방식을 구현했는데 TLE가 발생했다. 최적화를 했는데도 TLE여서 접근을 바꾸기로 했다.


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
90
91
92
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef unsigned long long int ull;
typedef long double ldb;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
struct Fenwick
{
    int tree[955555], C=450000;
    void init(void) { memset(tree, 0sizeof(tree)); }
    void update(int x, int v) { while(x<=C) { tree[x]+=v; x+=(x&-x); } }
    int query(int x) { int ret=0while(x) { ret+=tree[x]; x-=(x&-x); } return ret; }
    int rquery(int l, int r) { return query(r)-query(l-1); }
} T; 
 
int n, k, q;
int init[222222], ch[222222], ps[222222], res[222222];
int whi[222222], v[222222];
vector<int> POS;
 
int get_median(void)
{
    int lef, rig, mid, best;
    lef=1; rig=POS.size();
    while(lef<=rig)
    {
        mid=(lef+rig)/2;
        if(T.query(mid)>=(n-k+3)/2) best=mid, rig=mid-1;
        else lef=mid+1;
    }
    return POS[best-1];
}
 
void UPD(int vv, int t)
{
    int loc=lower_bound(POS.begin(), POS.end(), vv)-POS.begin()+1;
    T.update(loc, t); return;
}
 
void solve(void)
{
    int i, j, c=0, tt=0cin>>n>>k>>q; 
    tt=n-k+1; POS.clear();
    for(i=1 ; i<=n ; i++cin>>init[i];
    for(i=1 ; i<=q ; i++cin>>whi[i]>>v[i]; 
    for(i=1 ; i<=q ; i++) tt+=min(whi[i], n-k+1)-max(1, whi[i]-k+1)+1;
    POS.resize(tt); T.C=tt+5;
    for(i=1 ; i<=n ; i++) ch[i]=init[i];
    for(i=1 ; i<=n ; i++) ps[i]=ps[i-1]+init[i]; // ps[i] : i ~ i+k-1
    for(i=1 ; i<=n-k+1 ; i++) res[i]=ps[i+k-1]-ps[i-1];
    for(i=1 ; i<=n-k+1 ; i++) POS[c++]=res[i];
    for(i=1 ; i<=q ; i++)
    {
        for(j=max(1, whi[i]-k+1) ; j<=min(whi[i], n-k+1) ; j++
        {
            res[j]+=v[i]-ch[whi[i]];
            POS[c++]=res[j];
        }
        ch[whi[i]]=v[i];
    }
    sort(POS.begin(), POS.end());
    POS.erase(unique(POS.begin(), POS.end()), POS.end());
    for(i=1 ; i<=n ; i++) ch[i]=init[i];
    for(i=1 ; i<=n ; i++) ps[i]=ps[i-1]+init[i];
    for(i=1 ; i<=n-k+1 ; i++) res[i]=ps[i+k-1]-ps[i-1];
    for(i=1 ; i<=n-k+1 ; i++) UPD(res[i], 1); cout<<get_median()<<" ";
    for(i=1 ; i<=q ; i++)
    {
        for(j=max(1, whi[i]-k+1) ; j<=min(whi[i], n-k+1) ; j++
        {
            UPD(res[j], -1);
            res[j]+=v[i]-ch[whi[i]];
            UPD(res[j], 1);
        }
        ch[whi[i]]=v[i];
        cout<<get_median()<<" ";
    }
    for(i=0 ; i<=2*tt+20 ; i++) T.tree[i]=0;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc; T.init();
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


두 번째 방식은 priority_queue를 이용한다. 

$n-k+1$개의 수들 중 작은 절반을 관리하는 PQ와 큰 절반을 관리하는 PQ를 생각하자.

이를 관리하는 것은 어렵지 않은데, 다음을 반복하면 된다.

  • 원소 삭제시, 해당 원소를 포함하는 PQ에서 원소 삭제
  • 원소 추가시, 중앙값과 비교하여 적합한 PQ에 원소 추가
  • 중앙값 계산시, 큰 절반을 관리하는 PQ에서 최소 원소 찾기
  • 마지막으로, 양쪽 PQ의 원소 개수가 원하는 반반이 되도록 밸런스 맞추기

이제 문제는 PQ에서 원소를 삭제해야 한다는 것이다. 그런데 삭제 가능한 PQ가 있다!

나는 친구에게 들었는데, 대충 top()을 호출할 때 삭제된 얘들을 다 날리는 방식이다. 자세한 것은 코드 참고.


이 방식은 연산 자체가 훨씬 간단해서 더욱 빠르다. 제출 10번 채웠으면 억울해 죽을 뻔 ㅋㅋ


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
90
91
92
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef unsigned long long int ull;
typedef long double ldb;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
int inf=1e9;
struct iHeap 
{
    priority_queue<int> q, qr;
    void rset(void) { while(!q.empty()) q.pop(); while(!qr.empty()) qr.pop(); }
    inline int size(void) { return q.size()-qr.size(); }
    inline void rmv(int x) { qr.push(x); }
    inline int top()
    {
        while(q.size() && qr.size() && q.top() == qr.top()) { q.pop(); qr.pop(); }
        return q.size()?q.top():-inf;
    }
    inline void push(int x) { q.push(x); }
} A, B;
 
int n, k, q, asz, bsz;
int init[222222], ch[222222];
int ps[222222], res[222222];
int cv[222222];
int whi[222222], v[222222];
vector<int> POS;
 
void rmv(int x)
{
    if(x<=A.top()) { A.rmv(x); }
    else { B.rmv(-x); }
}
 
void ins(int x)
{
    if(x<=A.top()) { A.push(x); }
    else { B.push(-x);  } 
}
 
void housekeep(void)
{
    while(A.size()>asz)
    {
        ll t=A.top(); A.rmv(t);
        B.push(-t);
    }
    while(B.size()>bsz)
    {
        ll t=B.top(); B.rmv(t);
        A.push(-t);
    }
}
 
void solve(void)
{
    int i, j; cin>>n>>k>>q; A.rset(); B.rset();
    for(i=1 ; i<=n ; i++cin>>init[i];
    for(i=1 ; i<=q ; i++cin>>whi[i]>>v[i]; 
    for(i=1 ; i<=n ; i++) ch[i]=init[i];
    for(i=1 ; i<=n ; i++) ps[i]=ps[i-1]+init[i]; 
    for(i=1 ; i<=n-k+1 ; i++) { res[i]=ps[i+k-1]-ps[i-1]; cv[i]=res[i]; }
    sort(cv+1, cv+(n-k+1)+1);
    for(i=1 ; i<=(n-k+1)/2 ; i++) { A.push(cv[i]); }
    for(i=(n-k+1)/2+1 ; i<=(n-k+1) ; i++) { B.push(-cv[i]); }
    asz=(n-k+1)/2; bsz=(n-k+1)-asz;
    cout<<-B.top()<<" ";
    for(i=1 ; i<=q ; i++)
    {
        for(j=max(1, whi[i]-k+1) ; j<=min(whi[i], n-k+1) ; j++
        {
            rmv(res[j]);
            res[j]+=v[i]-ch[whi[i]];
            ins(res[j]);
        }
        ch[whi[i]]=v[i]; housekeep();
        cout<<-B.top()<<" ";
    }
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


4. 안전운전


가로선은 총합에 고려하지 않는다는 것을 늦게 읽어서 뇌절한 문제다. 

특정 $x$좌표 $T$에서 뒤집기를 시전했다고 가정하자. 그러면 그 후 경로의 모든 가로 좌표 $x$는 $2T-x$로 변환된다.


주어진 경로와 도로를 구간으로 쪼개서, 각 구간에서 경로/도로들의 가로 좌표가 고정되도록 하자.


이제 뒤집기 이전과 이후, 답에 더해지는 값들을 생각해보자.

  • 뒤집기 이전에는 기존 경로 그대로 간다. 여기서 더해지는 값은 미리 전처리 가능하다.
  • 뒤집는 가로선에서 $T$는 해당 가로선의 양 끝점 사이의 좌표가 된다.
  • 뒤집기 이후에는 가로 좌표 $x$가 $2T-x$로 변환된다. 이후 구간에서 기존 가로 좌표가 $x$고, 왼쪽/오른쪽 도로의 가로 좌표가 각각 $L, R$인 경우, $L \le 2T-x \le R$인 경우에만 구간의 세로 길이가 답에 더해진다. 이는 부등식은 $(L+x)/2 \le T \le (R+x)/2$과 같다.

이제 뒤집는 가로선의 위치를 고정하고 생각하자. 이 가로선의 위치를 위에서부터 아래로 순서대로 본다.

매우 커다란 가상의 세그먼트 트리를 하나 준비한다.

  • 각 구간에 대해, 세그트리의 구간 $[(L+x)/2, (R+x)/2]$에 해당 구간의 세로 길이만큼의 값을 더한다.
  • 가로선이 등장한 경우, 그 가로선의 범위를 $[x_l, x_r]$이라 하자. 이 구간에서 세그트리의 최댓값을 뽑는다.
  • 그 가로선 아래에서 얻어지는 값은 미리 전처리하였으므로, 이를 더한다.
  • 이렇게 해서 얻어지는 값이 답의 후보이다. 이를 위에서부터 아래로 내려가면서 반복한다.

물론, 커다란 가상의 세그먼트 트리는 단순히 좌표압축으로 단순 세그먼트 트리로 바꿀 수 있다.

이제 필요한 것은 구간에 값 추가 + 구간 최댓값을 지원하는 세그먼트 트리고, 이는 lazy propagation으로 가능하다.


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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef unsigned long long int ull;
typedef long double ldb;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
struct SegmentTree
{
    int tree[3588888];
    int lazy[3588888];
    void init(void) { memset(tree, 0sizeof(tree)); memset(lazy, 0sizeof(lazy)); }
    void workdown(int index, int s, int e)
    {
        if(s!=e)
        {
            lazy[index<<1]+=lazy[index];
            tree[index<<1]+=lazy[index];
            lazy[index<<1|1]+=lazy[index];
            tree[index<<1|1]+=lazy[index];
        }
        lazy[index]=0;
    }
    void update(int index, int s, int e, int l, int r, int v)
    {
        if(l>|| r<s) return
        workdown(index,s,e); 
        if(l<=&& e<=r) 
        {
            lazy[index]+=v;
            tree[index]+=v;
            return;
        }
        int m=(s+e)>>1;
        update(index<<1,s,m,l,r,v); 
        update(index<<1|1,m+1,e,l,r,v); 
        tree[index]=max(tree[index<<1], tree[index<<1|1]);
    }
    int eval(int index, int s, int e, int l, int r)
    {
        if(l>|| r<s) return -1e9;
        workdown(index, s, e);
        if(l<=&& e<=r) return tree[index];
        int m=(s+e)>>1;
        int ret=max(eval(index<<1,s,m,l,r), eval(index<<1|1,m+1,e,l,r)); 
        tree[index]=max(tree[index<<1], tree[index<<1|1]); 
        return ret; 
    }
} T;
 
int ans, cnt, L, R, M, fin;
int lidx, ridx, midx;
int cut[666666];
pair<intint> LF[222222], RG[222222], MM[222222];
vector<int> XP, RES;
int secL[666666], secR[666666];
int secM[666666], secV[666666];
 
void UPD(int u, int v, int er)
{
    u=lower_bound(RES.begin(), RES.end(), u)-RES.begin()+1;
    v=lower_bound(RES.begin(), RES.end(), v)-RES.begin()+1;
    T.update(11, RES.size(), u, v, er);
}
 
int QUERY(int u, int v)
{
    if(u>v) swap(u, v);
    u=lower_bound(RES.begin(), RES.end(), u)-RES.begin()+1;
    v=lower_bound(RES.begin(), RES.end(), v)-RES.begin()+1;
    return T.eval(11, RES.size(), u, v);
}
 
void solve(void)
{
    fio; ll i, j, x, y, u, v; cin>>L>>R>>M;
    XP.clear(); RES.clear(); cnt=0; ans=0;
    for(i=1, x=0, y=0 ; i<=L ; i++)
    {
        cin>>u>>v; x+=u; LF[i]=make_pair(y, x);
        XP.push_back(y); y+=v; fin=y;
    }
    for(i=1, x=0, y=0 ; i<=R ; i++)
    {
        cin>>u>>v; x+=u; RG[i]=make_pair(y, x); 
        XP.push_back(y); y+=v; fin=y;
    }
    for(i=1, x=0, y=0 ; i<=M ; i++)
    {
        cin>>u>>v; x+=u; MM[i]=make_pair(y, x); 
        XP.push_back(y); y+=v; fin=y;
    }
    sort(XP.begin(), XP.end()); 
    XP.erase(unique(XP.begin(), XP.end()), XP.end()); XP.push_back(fin);
    for(i=0, lidx=1, ridx=1, midx=1 ; i<XP.size() ; i++)
    {
        if(XP[i]==fin) break;
        while(lidx<=&& LF[lidx].first<=XP[i]) lidx++; lidx--;
        while(ridx<=&& RG[ridx].first<=XP[i]) ridx++; ridx--;
        while(midx<=&& MM[midx].first<=XP[i]) midx++; midx--;
        ++cnt; secL[cnt]=LF[lidx].second; secR[cnt]=RG[ridx].second;
        secM[cnt]=MM[midx].second; secV[cnt]=XP[i+1]-XP[i];
        RES.push_back((secL[cnt]+secM[cnt])/2); RES.push_back((secR[cnt]+secM[cnt])/2);
    }
    for(i=1 ; i<=cnt-1 ; i++)
    {
        if(secM[i]!=secM[i+1]) 
        {
            RES.push_back(secM[i]);
            RES.push_back(secM[i+1]);
        }
    }
    sort(RES.begin(), RES.end());
    RES.erase(unique(RES.begin(), RES.end()), RES.end());
    for(i=1 ; i<=cnt ; i++)
    {
        cut[i]=cut[i-1];
        if(secL[i]<=secM[i] && secM[i]<=secR[i]) cut[i]+=secV[i];
    }
    ans=cut[cnt];
    for(i=cnt ; i>=2 ; i--)
    {
        UPD((secL[i]+secM[i])/2, (secR[i]+secM[i])/2, secV[i]);
        if(secM[i]!=secM[i-1]) ans=max(ans, cut[i-1]+QUERY(secM[i-1], secM[i]));
    }
    for(i=0 ; i<=4*RES.size() ; i++) T.tree[i]=T.lazy[i]=0;
    cout<<ans; return;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc; T.init();
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs

5. 삼각형의 거리


볼록다각형의 경우 작년 ICPC 기출문제다. 그대로 제출하면 47점이다.


이제 본격적으로 문제를 해결해보자. 답에 대하여 이분탐색을 하자. 답이 $m$ 이하가 될 수 있는지 판별하자.

정점을 반시계 순서대로 $v_1, v_2, \cdots, v_n$이라고 하자. 

각 $i<j$에 대하여 $P_{i, j}$를 $v_i, v_{i+1}, \cdots , v_j$로 이루어진 다각형이라고 하자.

단, $v_i v_j$가 문제에서 주어진 다각형 내부에 완전히 위치해야 한다.


이제 $DP[i][j]$를 $P_{i, j}$를 지름을 $m$ 이하로 삼각분할 할 때, [$v_iv_j$를 포함하는 삼각형에서 가장 먼 삼각형까지의 거리]로 가능한 것 중 최솟값에 $1$을 더한 값이라고 하자. 만약 조건을 만족하는 삼각분할이 불가능하면 $\infty$라 하자.


$DP[i][j]$를 계산하기 위해, 점화식을 설계하자. $k \in [i+1, j-1]$을 하나 잡고 $v_i v_k v_j$가 정당한 삼각형인지를 판별하자.

만약 정당하다면, $v_iv_jv_k$와 $P_{i, k}$의 삼각분할, $P_{k, j}$의 삼각분할로 구성된 $P_{i, j}$의 삼각분할을 생각할 수 있다.

이 경우 지름은 $DP[i][k] + DP[k][j]$가 되며, 이 값이 $m$ 초과인 경우 실패라고 볼 수 있다.

성공이라면, $DP[i][j]$로 가능한 값 중 하나는 $\text{max}(DP[i][k], DP[k][j]) + 1$이 된다. 이를 업데이트 하자.


이렇게 되면 고려해야 할 $DP$ 상태는 $\mathcal{O}(n^2)$, 전이 시간복잡도도 $\mathcal{O}(n^3)$이다.


이제 본격적으로 기하적 부분을 처리해야 한다. 고통스러웠다 :(

$v_i v_k v_j$가 정당한 삼각형인지 판별하는 것은 각 변이 주어진 다각형 안에 속하는지만 판별해도 된다.

이제 선분 $v_iv_j$가 주어진 다각형 안에 속하는지 판별하는 방법을 생각해보자.

우선 다각형의 선분들 중 $v_i v_j$와 만나는 것은 없어야 한다. (끝점 제외)

이 판별이 끝나면, $v_i v_j$는 전부 다각형 내부에 속하거나 전부 다각형 외부에 속한다.

그러니, $v_iv_j$의 중점을 잡고 다각형 내부에 속하는지 판별하면 끝난다!

Point in Polygon은 ray casting algorithm으로 하면 되며, 설명은 여기를 참고하자. 


중점을 계산해야 하는데 소수점 나오면 기분이 나쁘니 모든 좌표를 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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef unsigned long long int ull;
typedef long double ldb;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
 
ll inf=1e9;
 
struct point_2d // ll
{
    ll x, y;
    point_2d() {}
    point_2d(ll x, ll y): x(x), y(y) {}
    bool operator==(const point_2d &t) { return x==t.x && y==t.y; }
    point_2d& operator+=(const point_2d &t) { x+=t.x; y+=t.y; return *this; }
    point_2d& operator-=(const point_2d &t) { x-=t.x; y-=t.y; return *this; }
    point_2d& operator*=(const ll t) { x*=t; y*=t; return *this; }
    point_2d& operator/=(const ll t) { x/=t; y/=t; return *this; }
    point_2d operator+(const point_2d &t) const { return point_2d(*this)+=t; }
    point_2d operator-(const point_2d &t) const { return point_2d(*this)-=t; }
    point_2d operator*(const ll t) const { return point_2d(*this)*=t; }
    point_2d operator/(const ll t) const { return point_2d(*this)/=t; }
    ll cross(const point_2d &t) const { return x*t.y-y*t.x; }
    ll cross(const point_2d &a, const point_2d &b) const { return (a-(*this)).cross(b-(*this)); }
};
 
bool inter_1d(ll a, ll b, ll c, ll d) 
{
    if(a>b) swap(a, b);
    if(c>d) swap(c, d);
    return max(a,c)<=min(b,d);
}
 
int sgn(ll x)
{
    if(x>0return 1;
    if(x==0return 0;
    if(x<0return -1;
}
 
bool check_inter(point_2d a, point_2d b, point_2d c, point_2d d)
{
    if(c.cross(a,d)==0 && c.cross(b,d)==0 && c.cross(a,b)==0// a, b, c, d colinear
        return inter_1d(a.x,b.x,c.x,d.x) && inter_1d(a.y,b.y,c.y,d.y);
    return sgn(a.cross(b,c))!=sgn(a.cross(b,d)) && sgn(c.cross(d,a))!=sgn(c.cross(d,b));
}
 
int n;
point_2d pt[333];
int isok[311][311];
int dp[311][311];
 
bool chk(int x)
{
    int i, j, k, inf=1e9;
    for(i=1 ; i<=n-1 ; i++)
    {
        for(j=1 ; j<=n-i ; j++)
        {
            if(i==1) { dp[j][j+i]=0continue; } 
            if(isok[j][j+i]==0) { dp[j][j+i]=inf; continue; }
            dp[j][j+i]=inf;
            for(k=j+1 ; k<=j+i-1 ; k++)
            {
                if(isok[j][k]==0 || isok[k][j+i]==0continue;
                if(dp[j][k]+dp[k][j+i]>x) continue;
                dp[j][j+i]=min(dp[j][j+i], max(dp[j][k], dp[k][j+i])+1);
            }
        }
    }
    if(dp[1][n]<5000return truereturn false;
}
 
bool inConcave(point_2d X)
{
    int i, cnt=0;
    point_2d Y; Y.x=inf+1; Y.y=X.y+1;
    for(i=1 ; i<=n ; i++if(pt[i]==X) return true;
    for(i=1 ; i<=n ; i++
        if(check_inter(pt[i], pt[i%n+1], X, Y)) cnt++;
    return cnt%2==1;
}
 
int chk(int u, int v)
{
    if(v==u+1 || (u==1 && v==n)) return 1;
    for(int i=1 ; i<=n ; i++)
    {
        if(i==|| i%n+1==|| i==|| i%n+1==v) continue;
        if(check_inter(pt[u], pt[v], pt[i], pt[i%n+1])) return 0;
    }
    point_2d TT; TT.x=(pt[u].x+pt[v].x)/2; TT.y=(pt[u].y+pt[v].y)/2;
    if(!inConcave(TT)) return 0return 1;
}
 
void solve(void)
{
    int i, j, k; cin>>n;
    for(i=1 ; i<=n ; i++cin>>pt[i].x>>pt[i].y;
    for(i=1 ; i<=n ; i++) pt[i]*=2;
    if(n==3) { cout<<0return; }
    if(n==4) { cout<<1return; }
    for(i=1 ; i<=n ; i++)
        for(j=i+1 ; j<=n ; j++)
            isok[i][j]=chk(i, j);
    int lef=0, rig=n, mid, best=n; 
    while(lef<=rig)
    {
        mid=(lef+rig)/2;
        if(chk(mid)) best=mid, rig=mid-1;
        else lef=mid+1;
    }
    cout<<best; return;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


'PS > 대회 후기' 카테고리의 다른 글

SCPC 2020 본선 후기  (5) 2020.11.09
ACM-ICPC Seoul Regional Preliminary 2020  (0) 2020.10.19
SCPC 2020 1차 예선 풀이  (0) 2020.08.22
APIO 2020 Open Contest  (0) 2020.08.22
ACM-ICPC Seoul Regional 2019  (0) 2019.11.11

아침에 일어나서 1, 2, 3번 풀고 밥 먹고 씻고 4, 5번 풀었다. 4번에서 뇌절한 거 빼고 괜찮았다.


대회가 대회인만큼 풀이는 최대한 자세하게, 코드까지 포함해서 작성한다.


1번. 다이어트


당연하게도 각 식당의 메뉴 중 가장 작은 $K$개를 먹어야 하는데, 짝을 잘 지어주는 것이 중요하다.

직관적으로 A 식당에서 칼로리가 많은 것을 B 식당에서 칼로리가 적은 것과 함께 먹어야 하고, 이는 실제로 정당한 풀이다.

증명하는 방법에는 여러 가지가 있을 수 있는데, 가장 쉬운 것은 exchange argument인 것 같다.

만약 $a_i < a_j$, $b_k < b_l$이 있어 $(a_i, b_k)$, $(a_j, b_l)$이 서로 짝을 이루고 있다면, 이를 $(a_i, b_l)$, $(a_j, b_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
31
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
typedef complex<double> base;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
const bool debug=false
const int mod=998244353;
 
ll a[222222], b[222222], n, k, ans;
 
void solve(void)
{
    int i; cin>>n>>k; ans=0;
    for(i=1 ; i<=n ; i++cin>>a[i]; sort(a+1, a+n+1);
    for(i=1 ; i<=n ; i++cin>>b[i]; sort(b+1, b+n+1);
    for(i=1 ; i<=k ; i++) ans=max(ans, a[i]+b[k+1-i]);
    cout<<ans;
}
 
int main(void)
{
    fio; int tc; cin>>tc;
    for(int i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


2번. 카드 게임


(초고난이도 문제를 제외한) 게임 문제는 대부분 전형적인 풀이를 갖는다. 가장 대표적인 풀이는 특정 상태를 "선공승", "후공승"으로 분류하고, 이 상태를 동적 계획법으로 계산하는 것이다. 특정 상태가 선공승이 되려면, 선공이 도달할 수 있는 상태 중 하나라도 후공승이면 된다. 반대로, 특정 상태가 후공승이 되려면, 선공이 도달할 수 있는 상태가 모두 선공승이면 된다. 이를 기반으로 동적계획법 풀이를 생각해보자. $dp[i][j]$란 더미 $X$에서 아래 $i$개의 카드, 더미 $Y$에서 아래 $j$개의 카드가 남았을 때 선공승인지, 후공승인지 여부라고 하자. 선공승이면 1, 아니면 0이다. 이제 각 상태에서 선공이 할 수 있는 움직임을 따져보자.


$(i, j)$라는 상태에 있다면, $X[t+1] + \cdots + X[i] \le k$을 만족하는 $t$에 대하여 $(t, j)$로 이동할 수 있다.

또한, $Y[t+1] + \cdots + Y[j] \le k$를 만족하는 $t$에 대하여 $(i, t)$로 이동할 수 있다. 

이러한 조건을 만족하는 $t$는 하나의 구간을 이루므로, 부분합 배열을 추가하여 각 DP 값을 $\mathcal{O}(1)$에 해결할 수 있다.


예를 들어, $X[t+1] + \cdots + X[i] \le k$를 만족하는 $t$가 $[u, i-1]$이라고 하자.

그러면 우리가 확인할 것은 $dp[u][j], dp[u+1][j], \cdots, dp[i-1][j]$ 중 $0$인 것이 존재하냐는 것이다.

이는 $dp[u][j]$부터 $dp[i-1][j]$까지의 합이 $i-u$와 같은지 파악하는 것과 같으며, 이는 부분합으로 가능하다.

$Y$의 경우도 마찬가지다. 또한, 이때 조건을 만족하는 $t$의 범위는 $X, Y$에 대한 부분합과 lower_bound 등으로 쉽게 구할 수 있다. 


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
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
typedef complex<double> base;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
const bool debug=false
const int mod=998244353;
 
int n, k, ans_1, ans_2;
int a[3333], b[3333];
int psa[3333], psb[3333];
int cuta[3333], cutb[3333];
int dp[3005][3005]; // first start, who wins?
int psx[3005][3005];
int psy[3005][3005];
 
void solve(void)
{
    int i, j, c; cin>>n>>k; ans_1=0, ans_2=0;
    for(i=1 ; i<=n ; i++cin>>a[i];
    for(i=1 ; i<=n ; i++cin>>b[i];
    for(i=1 ; i<=n ; i++) psa[i]=psa[i-1]+a[i];
    for(i=1 ; i<=n ; i++) psb[i]=psb[i-1]+b[i];
    for(i=0 ; i<=n ; i++)
    {
        cuta[i]=lower_bound(psa, psa+n+1, psa[i]-k)-psa;
        cutb[i]=lower_bound(psb, psb+n+1, psb[i]-k)-psb;
    }
    dp[0][0]=1; psx[0][0]=1; psy[0][0]=1;
    for(i=0 ; i<=n ; i++)
    {
        for(j=0 ; j<=n ; j++)
        {
            if(i==0 && j==0continue;
            bool canfin=false;
            if(i>=1)
            {
                int totlose=psx[i-1][j];
                if(cuta[i]>=1) totlose-=psx[cuta[i]-1][j];
                int totpos=i-cuta[i];
                if(totlose<totpos) canfin=true;
            }
            if(j>=1)
            {
                int totlose=psy[i][j-1];
                if(cutb[j]>=1) totlose-=psy[i][cutb[j]-1];
                int totpos=j-cutb[j];
                if(totlose<totpos) canfin=true;
            }
            if(canfin) dp[i][j]=1;
            else dp[i][j]=0;
            psx[i][j]=(i>=1?psx[i-1][j]:0)+dp[i][j];
            psy[i][j]=(j>=1?psy[i][j-1]:0)+dp[i][j];
        }
    }
    for(i=0 ; i<=n ; i++)
    {
        for(j=0 ; j<=n ; j++)
        {
            if(dp[i][j]) ans_1++;
            else ans_2++;
        }
    }
    cout<<ans_1<<" "<<ans_2;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


3번. 사다리 게임


$N$의 범위가 작으므로, 아예 $i$에서 $j$로 가기 위한 최소 간선 삭제 횟수를 $dp[i][j]$라고 정의하자. 이를 전부 구하는 것을 목표로 한다.

초깃값을 설정해야 하는데, $dp[i][i]=0$, $dp[i][j] = \infty$ ($j \neq i$) 가 된다. 이제 DP 업데이트를 어떻게 하는지 생각하자.


만약 $u, v$를 연결하는 간선이 있다면, $u$로 도착한 경로는 강제로 $v$로 가게 된다. 반대 역시 마찬가지.

그러니 $dp[i][u] = \text{min}(dp[i][u]+1, dp[i][v])$, $dp[i][v]=\text{min}(dp[i][v]+1, dp[i][u])$가 성립하게 된다.

물론, 이 업데이트를 순서대로 하면 안되고, 동시에 적용해야 한다. 그러니 업데이트가 간선 하나 당 $\mathcal{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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
typedef complex<double> base;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
const bool debug=false
const int mod=998244353;
 
ll ans=0;
int n, k, m, inf=1e9;
int dp[1511][1511];
 
void solve(void)
{
    int i, j, u, v, a, b; ans=0cin>>n>>k>>m;
    for(i=1 ; i<=n ; i++)
        for(j=1 ; j<=n ; j++)
            dp[i][j]=inf;
    for(i=1 ; i<=n ; i++) dp[i][i]=0;
    while(k--)
    {
        cin>>u>>v;
        for(i=1 ; i<=n ; i++)
        {
            a=dp[i][u]; b=dp[i][v];
            dp[i][u]=min(a+1, b);
            dp[i][v]=min(b+1, a);
        }
    }
    while(m--)
    {
        cin>>u>>v;
        if(dp[u][v]==inf) ans--;
        else ans+=dp[u][v];
    }
    cout<<ans;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


4번. 범위 안의 숫자


뭔가 지나치게 어렵게 푼 것 같다. 고생도 많이했다. 이상한 실수해서 제출횟수도 날리고 ㅋㅋ


우리가 해야 할 일은, 어떤 정수를 추가하고, 삭제하고, 길이 $m$ 구간에 속하는 정수의 최대 개수를 구하는 것이다.

여기서 첫 관찰은, 집합에 존재하는 정수로 시작하는 구간만을 생각해도 충분하다는 것이다. 이는 자명한 사실이므로 설명 생략.

먼저 집합에 등장할 수 있는 정수들을 전부 구하고, 이들을 정렬한 상태로 보관하자. 총 $\mathcal{O}(nk)$개가 있다.

이제 각 정수에 대하여 "그 정수로 시작하는 길이 $m$ 구간에 속한 정수의 개수"를 세그먼트 트리로 관리해보자.

특정 정수를 추가/삭제하면, 그에 따라 영향을 받는 값의 index는 하나의 구간을 이룬다. 이제 해결의 실마리가 나온다.

필요한 것은 구간에 1을 더하는 것, 1을 빼는 것, 그리고 전체의 최댓값을 구하는 연산이다. 모두 lazy propagation으로 가능하다.

하지만 이 풀이는 시간 초과가 난다. (아니라는 말도 있다) $nk$가 생각보다 크고, lazy propagation은 상수가 꽤 크기 때문이다.


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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
typedef complex<double> base;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
const bool debug=false
const int mod=998244353;
 
struct SegmentTree
{
    int tree[2288888];
    int lazy[2288888];
    void build(void) { memset(tree, 0sizeof(tree)); memset(lazy, 0sizeof(lazy)); }
    void workdown(int index, int s, int e)
    {
        if(s!=e)
        {
            lazy[index<<1]+=lazy[index];
            tree[index<<1]+=lazy[index];
            lazy[index<<1|1]+=lazy[index];
            tree[index<<1|1]+=lazy[index];
        }
        lazy[index]=0;
    }
    void update(int index, int s, int e, int l, int r, int v)
    {
        if(l>|| r<s) return
        workdown(index,s,e); 
        if(l<=&& e<=r) 
        {
            lazy[index]+=v;
            tree[index]+=v;
            return;
        }
        int m=(s+e)>>1;
        update(index<<1,s,m,l,r,v); 
        update(index<<1|1,m+1,e,l,r,v); 
        tree[index]=max(tree[index<<1], tree[index<<1|1]);
    }
    int eval(int index, int s, int e, int l, int r)
    {
        if(l>|| r<s) return -1e9;
        workdown(index, s, e);
        if(l<=&& e<=r) return tree[index]; int m=(s+e)>>1;
        int ret=max(eval(index<<1,s,m,l,r), eval(index<<1|1,m+1,e,l,r)); 
        tree[index]=max(tree[index<<1], tree[index<<1|1]); 
        return ret; 
    }
} ST;
 
string s; int c[51111];
int ans, n, m, k, pt[10];
 
vector<int> POS;
vector<int> RNG;
 
void ins(int x)
{
    POS.push_back(x);
    for(int i=0 ; i<k ; i++) POS.push_back(x-x%pt[i+1]+x%pt[i]+pt[i]);
}
 
void upd(int x, int v)
{
    int tt=lower_bound(POS.begin(), POS.end(), x)-POS.begin();
    ST.update(11, POS.size(), RNG[tt]+1, tt+1, v);
}
 
void rv(int loc, int v)
{
    int i, ini=0;
    for(i=max(1, loc-k+1) ; i<=max(1, loc-k+1)+k-1 ; i++) ini=10*ini+c[i]; upd(ini, v);
    for(i=max(1, loc-k+1)+k ; i<=min(loc+k-1, n) ; i++) { ini=10*(ini-((c[i-k]*pt[k-1])))+c[i]; upd(ini, v); }
}
 
void solve(void)
{
    int i, j, ini=0, et; ans=0
    cin>>n>>k>>m>>s;
    POS.clear(); RNG.clear(); ST.build(); 
    pt[0]=1for(i=1 ; i<=k ; i++) pt[i]=10*pt[i-1];
    for(i=1 ; i<=n ; i++) c[i]=s[i-1]-'0';
    for(i=1 ; i<=k ; i++) ini=10*ini+c[i]; ins(ini);
    for(i=k+1 ; i<=n ; i++) { ini=10*(ini-((c[i-k]*pt[k-1])))+c[i]; ins(ini); }
    sort(POS.begin(), POS.end()); POS.erase(unique(POS.begin(), POS.end()), POS.end()); RNG.resize(POS.size());
    for(i=0 ; i<POS.size() ; i++) RNG[i]=lower_bound(POS.begin(), POS.end(), POS[i]-m)-POS.begin();
    ini=0for(i=1 ; i<=k ; i++) ini=10*ini+c[i]; upd(ini, 1);
    for(i=k+1 ; i<=n ; i++) { ini=10*(ini-((c[i-k]*pt[k-1])))+c[i]; upd(ini, 1); }
    ans=max(ans, ST.eval(11, POS.size(), 1, POS.size()));
    for(i=1 ; i<=n ; i++
    {
         rv(i, -1); et=c[i]; c[i]=1
         rv(i, 1); ans=max(ans, ST.eval(11, POS.size(), 1, POS.size())); 
         rv(i, -1); c[i]=et; rv(i, 1); 
    }
    cout<<ans; return;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


그래서 최적화를 할 방법을 생각했다. 사실 가장 중요한 부분은, 각 업데이트 이후 확인해야 하는 구간이 많이 겹친다는 것이다. 

숫자를 1로 바꾸기 전, 초기 $n-k+1$개의 정수 중 실제로 변화하는 것은 많아야 $k$개다. 이 점을 위 코드는 활용하지 않는다.

그러니, 위 레이지 세그먼트 트리를 초기 $n-k+1$개의 정수 중 하나로 시작하는 구간에 대해서만 만들어주자. 이러면 크기가 $\mathcal{O}(nk)$에서 $\mathcal{O}(n)$으로 줄어든다. 이제 우리가 걱정해야 하는 것은 "새로 추가되는 정수로 시작되는 길이 $m$ 구간"에 속하는 정수의 개수를 구하는 것이다. 이 구간에 속하는 정수 역시 "기존 $n-k+1$개의 정수"와 "새로 추가되는 정수"로 구분될 수 있는데, 후자는 단순하게 $\mathcal{O}(k^2)$으로 계산해도 간단한 계산이므로 충분히 빠르다. 전자의 경우, 기존 $n-k+1$개 정수의 등장 횟수를 Fenwick Tree로 관리하면 간단하게 계산할 수 있다. 특정 구간 내에 존재하는 정수의 개수를 구하면 되므로, lower_bound와 Fenwick Tree의 구간 쿼리를 통해서 계산할 수 있다. 시간복잡도는 크게 다르지 않으나, 최적화가 꽤 많이 된 코드이므로 훨씬 빠르다.


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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
typedef complex<double> base;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
const bool debug=false
const int mod=998244353;
 
struct SegmentTree
{
    int tree[288888];
    int lazy[288888];
    void build(void) { memset(tree, 0sizeof(tree)); memset(lazy, 0sizeof(lazy)); }
    void workdown(int index, int s, int e)
    {
        if(s!=e)
        {
            lazy[index<<1]+=lazy[index];
            tree[index<<1]+=lazy[index];
            lazy[index<<1|1]+=lazy[index];
            tree[index<<1|1]+=lazy[index];
        }
        lazy[index]=0;
    }
    void update(int index, int s, int e, int l, int r, int v)
    {
        if(l>|| r<s) return
        workdown(index,s,e); 
        if(l<=&& e<=r) 
        {
            lazy[index]+=v;
            tree[index]+=v;
            return;
        }
        int m=(s+e)>>1;
        update(index<<1,s,m,l,r,v); 
        update(index<<1|1,m+1,e,l,r,v); 
        tree[index]=max(tree[index<<1], tree[index<<1|1]);
    }
    int eval(int index, int s, int e, int l, int r)
    {
        if(l>|| r<s) return -1e9;
        workdown(index, s, e);
        if(l<=&& e<=r) return tree[index]; int m=(s+e)>>1;
        int ret=max(eval(index<<1,s,m,l,r), eval(index<<1|1,m+1,e,l,r)); 
        tree[index]=max(tree[index<<1], tree[index<<1|1]); 
        return ret; 
    }
} ST;
 
 
struct Fenwick
{
    int tree[111111], C=50000;
    void rset(void) { memset(tree, 0sizeof(tree)); }
    void update(int x, int v) { while(x<=C) { tree[x]+=v; x+=(x&-x); } }
    int query(int x) { int ret=0while(x) { ret+=tree[x]; x-=(x&-x); } return ret; }
    int rquery(int l, int r) { return query(r)-query(l-1); }
} T; 
 
string s; int c[51111];
int ans, n, m, k, pt[10];
 
vector<int> POS;
vector<int> AT;
 
void ins(int x) { POS.push_back(x); }
 
void upd(int x, int v, int ex)
{
    int t_1=lower_bound(POS.begin(), POS.end(), x-m)-POS.begin()+1;
    int t_2=upper_bound(POS.begin(), POS.end(), x)-POS.begin();
    ST.update(11, POS.size(), t_1, t_2, v);
    if(ex) { T.update(t_2, v); }
}
 
void rv(int loc, int v, int ex)
{
    int i, ini=0; AT.clear();
    for(i=max(1, loc-k+1) ; i<=max(1, loc-k+1)+k-1 ; i++) ini=10*ini+c[i]; upd(ini, v, ex); if(v==1 && ex==0) AT.push_back(ini);
    for(i=max(1, loc-k+1)+k ; i<=min(loc+k-1, n) ; i++) { ini=10*(ini-((c[i-k]*pt[k-1])))+c[i]; upd(ini, v, ex); if(v==1 && ex==0) AT.push_back(ini); }
}
 
void do_fun_work(void)
{
    int i, j, ac;
    for(i=0 ; i<AT.size() ; i++)
    {
        int t_1=lower_bound(POS.begin(), POS.end(), AT[i])-POS.begin()+1;
        int t_2=upper_bound(POS.begin(), POS.end(), AT[i]+m)-POS.begin();
        ac=T.rquery(t_1, t_2);
        for(j=0 ; j<AT.size() ; j++if(AT[i]<=AT[j] && AT[j]<=AT[i]+m) ac++;
        ans=max(ans, ac);
    }
}
 
void solve(void)
{
    int i, j, ini=0, et; ans=0
    cin>>n>>k>>m>>s;
    POS.clear(); ST.build(); T.rset();
    pt[0]=1for(i=1 ; i<=k ; i++) pt[i]=10*pt[i-1];
    for(i=1 ; i<=n ; i++) c[i]=s[i-1]-'0';
    for(i=1 ; i<=k ; i++) ini=10*ini+c[i]; ins(ini);
    for(i=k+1 ; i<=n ; i++) { ini=10*(ini-((c[i-k]*pt[k-1])))+c[i]; ins(ini); }
    sort(POS.begin(), POS.end()); POS.erase(unique(POS.begin(), POS.end()), POS.end());
    ini=0for(i=1 ; i<=k ; i++) ini=10*ini+c[i]; upd(ini, 11);
    for(i=k+1 ; i<=n ; i++) { ini=10*(ini-((c[i-k]*pt[k-1])))+c[i]; upd(ini, 11); }
    ans=max(ans, ST.eval(11, POS.size(), 1, POS.size())); 
    for(i=1 ; i<=n ; i++)
    {
        rv(i, -11); et=c[i]; c[i]=1
         rv(i, 10); ans=max(ans, ST.eval(11, POS.size(), 1, POS.size())); do_fun_work();
         rv(i, -10); c[i]=et; rv(i, 11); 
    }
    cout<<ans;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs


5번. 우범 지역


평행이동을 통해서 점 $Q$를 원점으로 옮기고 시작하자. 원점을 기준으로 나머지 점들을 각도정렬하자.

컨벡스 헐 안에 $Q$가 있을 확률은 구하기 어렵지만, $Q$가 없을 확률은 구하기 상대적으로 쉽다.

$Q$가 없을 필요충분조건은, 결국 선택한 점들이 이루는 각도 범위가 180도 미만인 것이기 때문이다.

우선 이를 기반으로 답이 $0$인지 아닌지는 쉽게 판단할 수 있다. 이를 먼저 처리해주자.

이제 점들을 각도순으로 $P_1, P_2, \cdots,  P_n$이라 하고, 그 선택 확률을 $p_1, p_2, \cdots , p_n$이라 하자.

각도 범위가 180도 미만인 경우, "시계 방향으로 처음 선택된 정점"을 직관적으로 정의할 수 있다.

위 그림에서는 오른쪽 아래에 있는 정점이 "처음 선택된 정점"이다. 이제 $P_i$가 처음 선택된 정점이라고 하자.

그 후, $P_i$부터 $P_j$까지의 각도 범위가 180도 미만인 최대의 $j$를 선택하자. 여기서 index가 한 바퀴 돌 수 있음에 주의하자. 

이를 쉽게 짜기 위해서는, $P_1, P_2, \cdots , P_n$의 정보를 $P_{n+1} , P_{n+2}, \cdots,  P_{2n}$에 복사해서 사용하면 된다.

이제 이렇게 되면 $P_{j+1}, P_{j+2}, \cdots , P_{i+n-1}$은 사용될 수 없다. 즉, 우리가 만족해야 하는 조건은 다음과 같다.

  • 우선 $P_i$를 선택해야 한다. 그 확률은 $p_i$이다. 

  • $P_{j+1}$부터 $P_{i+n-1}$은 사용되지 않는다. 그 확률은 $(1-p_{j+1}) \cdots (1-p_{i+n-1})$이다.

그러니 두 확률의 곱을 "Q가 컨벡스 헐에 포함되지 않을 확률"에 추가하면 된다. 

또한, 모든 점이 선택되지 않을 확률 역시 추가되어야 한다. 이러면 모든 확률을 중복없이 다루게 된다.

이 풀이의 매우 많은 부분은 $Q$와 $P_i, P_j$가 한 직선 위에 있지 않다는 사실에 기반한다.


위 계산을 효율적으로 할 방법을 생각해보자. 우선 $j$를 계산하는 것은 inchworm의 방식으로 쉽게 할 수 있다.

또한, $1-p$의 연속된 곱을 효율적으로 계산하는 것은 간단한 세그먼트 트리로 할 수 있다.


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
#include <bits/stdc++.h>
#define fio ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
typedef long long int ll;
typedef long double ldb;
typedef complex<double> base;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
const bool debug=false
const int mod=998244353;
 
ll n, qx, qy, C=210000; ldb tree[444444];
ldb ans, p[222222];
pair< pair<ll, ll>, ldb> PT[222222];
 
ll cross(const pair<ll, ll> &a, const pair<ll, ll> &b) { return a.first*b.second-a.second*b.first; }
 
bool cmp(const pair< pair<ll, ll>, ldb> &X, const pair< pair<ll, ll>, ldb> &Y)
{
    if((X.first>make_pair(0LL, 0LL)) ^ (Y.first>make_pair(0LL, 0LL))) return X.first > Y.first;
    return cross(X.first, Y.first) > 0;
}
 
void update(int loc, ldb t)
{
    loc+=C; tree[loc]=t;
    for( ; loc>1 ; loc>>=1) tree[loc>>1]=tree[loc]*tree[loc^1];
}
 
ldb query(int l, int r)
{
    ldb ret=1.0;
    for(l+=C, r+=C ; l<r ; l>>=1, r>>=1)
    {
        if(l&1) ret=ret*tree[l++];
        if(r&1) ret=ret*tree[--r];
    }
    return ret;
}
 
ldb getR(int L, int R)
{
    if(R<=n) return query(1, L)*query(R+1, n+1);
    if(R>n) return query(R-n+1, L);
}
 
void solve(void)
{
    fio; ll i, j; cin>>n; ans=0.0;
    memset(tree, 0sizeof(tree));
    for(i=1 ; i<=n ; i++cin>>PT[i].first.first;
    for(i=1 ; i<=n ; i++cin>>PT[i].first.second;
    for(i=1 ; i<=n ; i++cin>>PT[i].second;
    cin>>qx>>qy;
    for(i=1 ; i<=n ; i++) PT[i].first.first-=qx;
    for(i=1 ; i<=n ; i++) PT[i].first.second-=qy;
    sort(PT+1, PT+n+1, cmp);
    if(cross(PT[1].first, PT[n].first) > 0) { cout<<0.0return; }
    for(i=1 ; i<=n ; i++) PT[i+n]=PT[i];
    for(i=1 ; i<=2*n ; i++) update(i, 1.0-PT[i].second);
    for(i=1, j=1 ; i<=n ; i++)
    {
        while(j<=2*&& cross(PT[i].first, PT[j].first)>=0) j++; j--;
        ans+=PT[i].second*getR(i, j);
    }    
    ldb tt=1.0;
    for(i=1 ; i<=n ; i++) tt=(tt*(1.0-PT[i].second));
    ans=1.0-ans-tt;
    cout<<fixed<<setprecision(15)<<ans;
}
 
int main(void)
{
    fio; int i, tc; cin>>tc;
    for(i=1 ; i<=tc ; i++)
    {
        cout<<"Case #"<<i<<"\n";
        solve(); cout<<"\n";
    }
    return 0;
}
cs

 


'PS > 대회 후기' 카테고리의 다른 글

ACM-ICPC Seoul Regional Preliminary 2020  (0) 2020.10.19
SCPC 2020 2차 예선 풀이  (1) 2020.09.05
APIO 2020 Open Contest  (0) 2020.08.22
ACM-ICPC Seoul Regional 2019  (0) 2019.11.11
나는 코더다 2018 송년대회  (5) 2018.12.22

솔직히 아쉽긴 한데 그래도 완전 망한 성적은 아니어서 만족하는 중. C를 못 푼건 그렇다치고 B는 100 맞았어야 했다.

중간에 바빠서 정신 없었던 것도 있지만 어쨌든 B에 대한 접근이 정해와 조금 많이 떨어져 있어서 100은 못 먹었을 듯.


A. 벽 칠하기


각 길이 $m$ 구간이 "칠하는 것이 가능한 구간"임을 판별할 수 있다면, 그 후의 계산은 간단한 그리디 알고리즘으로 할 수 있다.

이제 이걸 판단하는 것이 중요한데, 이제부터 색깔 $i$를 좋아하는 사람의 집합을 $S_i$라 하자.

그럼 구간 $[i, i+m)$이 색칠 가능한 것은, $t \in S_{C[i]}$, $t+1 \in S_{C[i+1]}$, $\cdots$, $t+m-1 \in S_{C[i+m-1]}$을 만족하는 정수 $t$가 존재하는 것이다. 

모든 인덱스는 $\pmod{m}$으로 본다. 물론, 조건 판별을 이대로하면 고통스러울 것이 뻔하다. 그러니 접근을 바꿔보자.

생각해보면, 저 인덱스들의 차이는 $i-t$로 동일하다는 것을 알 수 있다. 그러니 이를 활용하면 뭔가 보일 것 같다.

즉, $u \in S_{C[v]}$에 대하여 $v-u$의 카운트를 $1$씩 증가시키면, 카운트가 $m$인 정수가 존재하는지 판별하면 된다.

이제 이걸 효율적으로 관리할 방법을 생각해보자. $[0, m)$에서 시작되는 구간을 오른쪽으로 움직여보자.

$[i. i+m)$에서 $[i+1, i+m+1)$로 가려면, $S_{C[i]}$에 있는 정보를 제거하고 $S_{C[i+m]}$의 정보를 추가해야 한다.

우리가 지원해야 하는 연산은 값 하나를 바꾸고, 전체의 최댓값을 뽑아내는 것이다. 이는 세그먼트 트리로 할 수 있다.

또한, 각 $i$에 대하여 $S_{C[i]}$는 $\sqrt{400000}$ 이하이므로, 업데이트 횟수가 $N \sqrt{400000}$ 이하가 된다.

그런데 내 경험상 세그먼트 트리를 붙이면 TLE가 나고, 로그를 떼어내야 AC를 받을 수 있다.

로그를 떼는 방법은 간단한데, 필요한 것이 최댓값이 $m$인지만을 확인하는 것임을 사용하면 된다.

그냥 $c[x]$를 카운트가 $x$인 정수의 개수, $cnt[x]$를 $x$의 카운트 값이라고 정의하면 업데이트가 상수 시간에 된다. 


B. 자매 도시


Subtask 1은 각 연결컴포넌트가 path 아니면 cycle이다. path이면 당연히 실패하며, cycle이면 그 중 간선 길이 최댓값이다.


Subtask 2는 그래프가 매우 특수한 형태를 갖는다. 경우를 나눠서 생각하면 간선 길이 중 최소 3~4개 정도만 생각하면 된다는 사실을 알 수 있다. 

이는 원하는 방식으로 적당히 구현하면 된다. 나는 멀티셋 썼다. 근데 은근 케이스 생각하기 헷갈리긴 했다 ㅋㅋ


Subtask 3, 4를 위해서는 간단한 관찰이 필요한데, 바로 필요충분조건이 연결성 및 (사이클 존재 또는 차수 3 이상 정점 존재)라는 것이다.

물론, 사이클이 단순 사이클이 아니면 차수 3 이상 정점은 무조건 존재하게 되어있다. 이제 생각하기 편하다.

이제 문제를 각 쿼리당 $\mathcal{O}(N+M)$ 시간에 해결할 수 있다. 간선을 작은 것부터 순서대로 이으면서, disjoint set으로 위 사실들을 관리하면 된다. 


Subtask 5를 위해서 나는 트리 DP를 활용했다. 오프라인 풀이가 너무 자명해서 small-to-large 같은 게 먹히나 생각을 했었는데, 뭔가 아닌 것 같아서 트리를 긁기로 했다. 근데 저 방향이 정해여서 너무 아쉽다 ㅋㅋㅋ 트리라도 맞은 걸 다행이라고 생각해야겠다.


여기서 좋은 점은 사이클 존재의 경우를 생각하지 않아도 된다는 것이다. 연결성과 차수 3 이상 정점만 다루면 된다.

연결성을 위한 "최대 간선 길이의 최솟값"과, 차수 3 이상 정점을 위한 "최대 간선 길이의 최솟값"을 구한 뒤 최댓값을 취하자.


첫 번째 문제는 어렵지 않고, 잘 알려진 편이다. MST를 찾아주고, 거기서 경로 최댓값을 찾으면 충분하기 때문이다.

그러니까 단순하게 크루스칼 + Sparse Table을 열심히 구현하면 이 부분을 큰 문제 없이 해결할 수 있다.


두 번째 문제는 엄청 어려운 건 아닌 것 같지만 트리 DP 근본이 없는 수준인 나에게는 힘들었다.

우선 차수 3 이상 정점이 어디서 나올 것인가에 대해서 논의를 할 필요가 있다. 쿼리로 들어온 정점이 $X, Y$라고 하자. 

 

우선 공통된 것이 있다. $X$와 $Y$ 사이의 경로에서 차수 3 이상 정점이 나올 수 있다.

이 정점들은 (단, $X, Y$ 제외) $X, Y$가 연결된 시점에서 이미 차수가 2 이상인 것이 보장되므로, 간선 하나만 더 그으면 끝이다.

여기서 생각을 해보면 중요한 것은 각 정점에서 뻗어나오는 "3번째로 길이가 작은 간선의 길이"가 된다.

그러니 첫 번째 문제와 같은 방법으로 Sparse Table을 적용하면 이 경우를 해결할 수 있다. 


그런데 $X, Y$는 조금 다른 점이 있다. 밑으로 내려갈 수도 있기 때문이다! 잠시 $Z = \text{lca}(X, Y)$라고 하자.

$Z \neq X$라면, 차수 3 정점을 찾기 위해서 $X$에서 트리를 내려가 정점 $W$에 도착한 뒤, $W$에서 가지를 2개 치는 방법이 있다.

이 경우 필요한 최대 길이는 $X$에서 $W$로 내려가는 길이 중 최대, 그리고 $W$의 가지 중 두 번째로 작은 것이 되겠다.

이러한 경로로 가능한 것 중 필요한 최대 길이의 최솟값을 구해야 하는데, 이는 트리 DP로 미리 전처리를 할 수 있다.

$Z \neq Y$인 경우에도 이러한 계산을 하고, 이를 쿼리를 답할 때에 고려해야 한다. 


마지막으로, $Z=X$거나 $Z=Y$인 경우를 생각할 필요가 있다. 이 경우에는 위로 올라갈 수 있다!

앞선 경우와 큰 차이는 없지만, 개인적으로는 조금 헷갈렸다. 비슷한 스타일의 DP를 한 번 더 해주면 된다.


매우 고통스러운 풀이였는데, 이걸 어쨌든 짜서 서브태스크를 긁었다는 게 다행이다. 솔직히 틀릴 것 같았다.


이 풀이를 일반적인 경우로 확장하려고 했는데, 단순 사이클을 다루는 것이 어마어마하게 힘들더라 ㅠㅠ   


C. 즐거운 행로


Subtask 1, 2는 우선 트리 전체의 구조를 $N^2$번의 쿼리로 구한다. (모든 거리를 알면 복원은 자명)

그 후, 트리의 지름을 잡고 가능한 정점 중 가장 먼 곳을 반복적으로 사용하면 된다. 


Subtask 3은 이 과정을 전형적인 이진트리에서 하면 된다. 위 과정을 그대로 적용하되, 트리의 높이가 작다는 점을 이용한다.

그냥 각 정점에 대해서 "내 왼쪽에 있는 정점" / "내 오른쪽에 있는 정점"의 집합을 관리하면 된다. 

'PS > 대회 후기' 카테고리의 다른 글

SCPC 2020 2차 예선 풀이  (1) 2020.09.05
SCPC 2020 1차 예선 풀이  (0) 2020.08.22
ACM-ICPC Seoul Regional 2019  (0) 2019.11.11
나는 코더다 2018 송년대회  (5) 2018.12.22
NYPC 2018 본선 복기 및 후기  (8) 2018.10.28