<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>PS 이야기</title>
    <link>https://mwchun.tistory.com/</link>
    <description>Problem Solving 공부를 하다</description>
    <language>ko</language>
    <pubDate>Sat, 11 Apr 2026 13:53:15 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>전명우</managingEditor>
    <image>
      <title>PS 이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/807845/attach/ab5782952f0541a0b16cbfa52a1264e8</url>
      <link>https://mwchun.tistory.com</link>
    </image>
    <item>
      <title>Bostan Mori 알고리즘</title>
      <link>https://mwchun.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Bostan Mori 알고리즘은 2020년 8월 Alin Bostan과 Ryuhei Mori가 작성한 &lt;a href=&quot;https://arxiv.org/abs/2008.08822&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 논문&lt;/a&gt;에 소개되어 있는 선형점화식을 가지는 수열의 $N$ 번째 항을 빠르게 구하는 알고리즘이다. Bostan Mori 알고리즘 이외에 선형점화식을 가지는 수열의 $N$ 번째 항을 빠르게 구하는 방법은 &lt;a href=&quot;https://blog.myungwoo.kr/148&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글&lt;/a&gt;을 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$D_0, D_1, \cdots, D_{k-1}$과 $c_1, c_2, \cdots, c_k$가 주어졌을 때, $i \ge k$인 $D_i$를 다음과 같은 선형점화식으로 구할 수 있다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$D_i = \sum_{j=1}^{k}{c_jD_{i-j}} = c_1D_{i-1} + c_2D_{i-2} + \cdots + c_kD_{i-k}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 선형점화식이 주어졌을 때, $D_N$을 Bostan Mori 알고리즘을 통해 구하는 방법을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$D$의 각 원소를 계수로 가지는 생성함수 $F$와 점화식을 바탕으로 만들어진 다항함수 $Q$가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$F(x) = \sum_{n=0}^{\infty}{D_nx^n}\\Q(x) = 1-c_1x-c_2x^2-\cdots-c_kx^k$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, $F(x) = \frac{P(x)}{Q(x)}$를 만족하는 다항함수 $P$가 존재한다는 것과 $P$의 차수가 $k$ 보다 작다는 것을 보일 수 있다. $P(x) = F(x)Q(x)$이고, $i \ge k$인 $i$에 대해 $P(x)$의 $x^i$ 항의 계수를 구해보면 $D_i - c_1D_{i-1} - c_2D_{i-2} - \cdots - c_kD_{i-k} = 0$이다. 즉, $P(x)$의 차수는 $k$ 보다 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구하고자 하는 것은 $N$ 번째 항인 $D_N$이다. 이는 $F(x) = \frac{P(x)}{Q(x)}$의 $x^N$ 항의 계수이다. 이를 다음과 같은 수식으로 표현하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\left[x^N\right]\frac{P(x)}{Q(x)}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 분수의 분자와 분모 모두에 $Q(-x)$를 곱해보자. 즉, $\frac{P(x)}{Q(x)} = \frac{P(x)Q(-x)}{Q(x)Q(-x)}$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, $Q(x)$의 차수는 $k$이다. $Q(x)Q(-x)$는 짝함수이기 때문에 $V(x^2)$으로 나타낼 수 있으며 $V$의 차수는 $k$ 이하다. $P(x)Q(-x) = U(x)$로 쓰자. $P(x)$의 차수는 $k$ 보다 작기 때문에 $U(x)$의 차수는 $2k$ 보다 작다. $U(x)$를 홀수차항과 짝수차항으로 $U(x) = U_e(x^2) + xU_o(x^2)$처럼 나누자. 여기서 $U_e(x)$와 $U_o(x)$의 차수는 $k$ 보다 작다. 이를 통해 식을 다음처럼 정리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\frac{P(x)}{Q(x)} = \frac{P(x)Q(-x)}{Q(x)Q(-x)} = \frac{U_e(x^2)}{V(x^2)} + x\frac{U_o(x^2)}{V(x^2)}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유리함수 $P/Q$의 $x^N$ 항의 계수는 다음과 같이 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\left[ x^N \right]\frac{P(x)}{Q(x)} = \begin{cases}&lt;br /&gt;\left[ x^{\frac{N}{2}} \right] \frac{U_e(x)}{V(x)}, &amp;amp; \text{if $N$ is even,}\\&lt;br /&gt;\left[ x^{\frac{N-1}{2}} \right] \frac{U_o(x)}{V(x)}, &amp;amp; \text{otherwise}&lt;br /&gt;&amp;nbsp;\end{cases}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 분자의 차수가 $k$보다 작고, 분모의 차수가 $k$ 이하인 유리함수 $P/Q$의 $x^N$ 항의 계수를 구하기 위해 같은 조건을 가지고 있는 또다른 유리함수의 $x^{\lfloor N/2 \rfloor}$의 계수를 구하도록 차원을 줄였다. 이처럼 두 번의 다항식 곱셈으로 구하고자 하는 항의 차수를 반으로 줄일 수 있다. 이를 상수항의 계수를 구할 때까지 반복한다. 유리함수 $P/Q$의 상수 계수는 $P(0)/Q(0)$으로 바로 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 방법으로 다항식의 곱셈을 구현하면 $O(k^2)$ 시간복잡도가 되고, FFT나 NTT를 통해 다항식의 곱셈을 구현하면 $O(k \lg k)$ 시간복잡도가 된다. 따라서, 전체 시간복잡도는 $O(k^2 \lg N)$ 혹은 $O(k \lg k \lg N)$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연습 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://boj.kr/15572&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://boj.kr/15572&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 MOD 값이 1999이므로 NTT를 사용할 수 없다. $k$의 최댓값이 1000으로 크지 않으므로 $O(k^2 \lg N)$ 시간복잡도로 해결할 수 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1669791672456&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;15572번: 블록 4&quot; data-og-description=&quot;여러 가지 블록들을 이용하여 직사각형 모양을 만들려고 한다. 우리에게는&amp;nbsp;1&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록,&amp;nbsp;2&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록, ...,&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록이 무한하게 있다. 이 블록들을 사용하여&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;M&amp;nbsp;모양을 만들고 &quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://boj.kr/15572&quot; data-og-url=&quot;https://www.acmicpc.net/problem/15572&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXjQVj/hyQJAXnYOB/kwhsNZ1htBkIY2lnQva7Ik/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://boj.kr/15572&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://boj.kr/15572&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXjQVj/hyQJAXnYOB/kwhsNZ1htBkIY2lnQva7Ik/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;15572번: 블록 4&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;여러 가지 블록들을 이용하여 직사각형 모양을 만들려고 한다. 우리에게는&amp;nbsp;1&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록,&amp;nbsp;2&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록, ...,&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록이 무한하게 있다. 이 블록들을 사용하여&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;M&amp;nbsp;모양을 만들고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://boj.kr/13725&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://boj.kr/13725&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MOD 값이 $104857601 = 25 \times 2^{22}+1$ 로 NTT를 쓸 수 있다. 원시근은 2부터 차례대로 구해보면 되는데, 3이 원시근이 된다. $k$의 최댓값이 30000으로 크기 때문에 $O(k \lg k \lg N)$ 시간복잡도로 문제를 해결해야 한다.&lt;/p&gt;
&lt;figure id=&quot;og_1669791817117&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;13725번: RNG&quot; data-og-description=&quot;첫째 줄에 k와 N (1 &amp;le; k &amp;le; 30,000, 1 &amp;le; N &amp;le; 1018)이 주어진다. 둘째 줄에는 A1, A2, ..., Ak가 셋째 줄에는 C1, C2, ..., Ck가 주어진다. (0 &amp;le; Ai, Ci &amp;lt; 104857601)&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://boj.kr/13725&quot; data-og-url=&quot;https://www.acmicpc.net/problem/13725&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBRPcL/hyQJA4chmU/Dnyz5lOG9CmvrlhoJdt5m1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://boj.kr/13725&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://boj.kr/13725&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBRPcL/hyQJA4chmU/Dnyz5lOG9CmvrlhoJdt5m1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;13725번: RNG&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 k와 N (1 &amp;le; k &amp;le; 30,000, 1 &amp;le; N &amp;le; 1018)이 주어진다. 둘째 줄에는 A1, A2, ..., Ak가 셋째 줄에는 C1, C2, ..., Ck가 주어진다. (0 &amp;le; Ai, Ci &amp;lt; 104857601)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 BOJ 13725 RNG 문제를 해결하는 코드이며, mod int와 NTT를 이용한 다항식 템플릿을 포함한 코드다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

template &amp;lt;int MOD&amp;gt;
struct MINT{
	int v{};
	MINT() = default;
	constexpr MINT(lld v_){
		v = -MOD &amp;lt;= v_ &amp;amp;&amp;amp; v_ &amp;lt; MOD ? v_ : v_ % MOD;
		if (v &amp;lt; 0) v += MOD;
	}

	friend istream&amp;amp; operator &amp;gt;&amp;gt; (istream&amp;amp; is, MINT&amp;amp; a){ lld t; is &amp;gt;&amp;gt; t; a = MINT(t); return is; }
	friend ostream&amp;amp; operator &amp;lt;&amp;lt; (ostream&amp;amp; os, const MINT&amp;amp; a){ return os &amp;lt;&amp;lt; a.v; }
	bool operator == (const MINT&amp;amp; ot)const{ return v == ot.v; }
	bool operator != (const MINT&amp;amp; ot)const{ return v != ot.v; }
	static constexpr MINT pow(MINT a, lld b){
		MINT ret = 1;
		for (;b;b&amp;gt;&amp;gt;=1,a*=a) if (b&amp;amp;1) ret *= a;
		return ret;
	}
	static constexpr MINT inv(const MINT&amp;amp; t){ return pow(t, MOD-2); }
	constexpr MINT operator - ()const{ return MINT(-v); }
	constexpr MINT&amp;amp; operator += (const MINT&amp;amp; ot){ if ((v += ot.v) &amp;gt;= MOD) v -= MOD; return *this; }
	constexpr MINT&amp;amp; operator -= (const MINT&amp;amp; ot){ if ((v -= ot.v) &amp;lt; 0) v += MOD; return *this; }
	constexpr MINT&amp;amp; operator *= (const MINT&amp;amp; ot){ v = (lld)v*ot.v%MOD; return *this; }
	constexpr MINT&amp;amp; operator /= (const MINT&amp;amp; ot){ *this *= inv(ot); return *this; }
	constexpr MINT operator + (const MINT&amp;amp; ot)const{ return MINT(*this) += ot; }
	constexpr MINT operator - (const MINT&amp;amp; ot)const{ return MINT(*this) -= ot; }
	constexpr MINT operator * (const MINT&amp;amp; ot)const{ return MINT(*this) *= ot; }
	constexpr MINT operator / (const MINT&amp;amp; ot)const{ return MINT(*this) /= ot; }
	constexpr operator int32_t ()const{ return v; }
	constexpr operator int64_t ()const{ return v; }
};

template &amp;lt;int prime = (119&amp;lt;&amp;lt;23)+1, int primitive_root = 3&amp;gt;
struct FFT{
	using T = MINT&amp;lt;prime&amp;gt;;
	static constexpr int P = prime;
	static constexpr int PR = primitive_root;
	static constexpr int PRI = T::inv(PR);
	static constexpr int MSZ = (P - 1) &amp;amp; (1 - P);
	static constexpr int W = T::pow(PR, (P - 1) / MSZ);
	static constexpr int WI = T::pow(PRI, (P - 1) / MSZ);

	static void fft(int sz, vector&amp;lt;T&amp;gt;&amp;amp; a, bool inv){
		T w = T::pow(inv ? WI : W, MSZ / sz);
		for (int ssz=sz&amp;gt;&amp;gt;1;ssz;ssz&amp;gt;&amp;gt;=1){
			for (int i=0;i&amp;lt;sz;i+=ssz&amp;lt;&amp;lt;1){
				T wt = 1;
				for (int j=0;j&amp;lt;ssz;++j){
					T&amp;amp; lft = a[i + j];
					T&amp;amp; rgh = a[i + j + ssz];
					T lftold = lft;
					lft += rgh;
					rgh = (lftold - rgh) * wt;
					wt *= w;
				}
			}
			w *= w;
		}
		if (inv){
			T invsz = T::inv(sz);
			for (int i=0;i&amp;lt;sz;i++) a[i] *= invsz;
		}
		for (int j=0,i=1;i&amp;lt;sz;i++){
			int dg = sz&amp;gt;&amp;gt;1;
			for (;j&amp;amp;dg;dg&amp;gt;&amp;gt;=1) j ^= dg;
			j ^= dg;
			if (i &amp;lt; j) swap(a[i], a[j]);
		}
	}

	static vector&amp;lt;T&amp;gt; convolution(const vector&amp;lt;T&amp;gt;&amp;amp; a, const vector&amp;lt;T&amp;gt;&amp;amp; b){
		int sz = a.size() + b.size();
		int fftsz = sz;
		while (fftsz &amp;amp; (fftsz - 1)) fftsz += fftsz &amp;amp; -fftsz;
		vector&amp;lt;T&amp;gt; av(fftsz);
		vector&amp;lt;T&amp;gt; bv(fftsz);
		for (int i=0,ilim=size(a);i&amp;lt;ilim;i++) av[i] = a[i];
		for (int i=0,ilim=size(b);i&amp;lt;ilim;i++) bv[i] = b[i];
		fft(fftsz, av, false);
		fft(fftsz, bv, false);
		for (int i=0;i&amp;lt;fftsz;i++) av[i] *= bv[i];
		fft(fftsz, av, true);
		return av;
	}
};

// alternatives
using FFT1 = FFT&amp;lt;1'012'924'417, 5&amp;gt;; // (483&amp;lt;&amp;lt;21)+1
using FFT2 = FFT&amp;lt;1'004'535'809, 3&amp;gt;; // (479&amp;lt;&amp;lt;21)+1

template &amp;lt;int prime = (119&amp;lt;&amp;lt;23)+1, int primitive_root = 3&amp;gt;
struct Poly{
	using T = MINT&amp;lt;prime&amp;gt;;
	using fft_t = FFT&amp;lt;prime, primitive_root&amp;gt;;
	vector&amp;lt;T&amp;gt; a;
	Poly() = default;
	Poly(T a0): a(1, a0){ normalize(); }
	Poly(const vector&amp;lt;T&amp;gt;&amp;amp; a): a(a) { normalize(); }

	int size()const{ return a.size(); }
	int deg()const{ return a.size()-1; }
	T&amp;amp; operator [](int i){ return a[i]; }
	typename vector&amp;lt;T&amp;gt;::const_iterator begin()const{ return a.begin(); }
	typename vector&amp;lt;T&amp;gt;::const_iterator end()const{ return a.end(); }
	void push_back(const T&amp;amp; v){ a.push_back(v); }
	template&amp;lt;typename... Args&amp;gt; void emplace_back(Args&amp;amp;&amp;amp;... args){ a.emplace_back(args...); }
	void pop_back(){ a.pop_back(); }
	void resize(int n){ a.resize(n); }

	void normalize(){ while (!a.empty() &amp;amp;&amp;amp; a.back() == T(0)) a.pop_back(); }

	Poly reversed()const{ auto b = a; reverse(b.begin(), b.end()); return b; }
	Poly trim(int n)const{ return vector&amp;lt;T&amp;gt;(a.begin(), a.begin() + min(n, size())); }
	Poly inv(int n){
		Poly q(T(1) / a[0]);
		for (int i=1;i&amp;lt;n;i&amp;lt;&amp;lt;=1){
			Poly p = Poly(2) - q*trim(i&amp;lt;&amp;lt;1);
			q = (p * q).trim(i&amp;lt;&amp;lt;1);
		}
		return q.trim(n);
	}

	Poly&amp;amp; operator *= (const T&amp;amp; x){ for (T&amp;amp; v: a) v *= x; normalize(); return *this; }
	Poly&amp;amp; operator /= (const T&amp;amp; x){ return *this *= T(1) / x; }
	Poly&amp;amp; operator += (const Poly&amp;amp; ot){
		if (size() &amp;lt; ot.size()) a.resize(ot.size());
		for (int i=0;i&amp;lt;ot.size();i++) a[i] += ot.a[i];
		normalize();
		return *this;
	}
	Poly&amp;amp; operator -= (const Poly&amp;amp; ot){
		if (size() &amp;lt; ot.size()) a.resize(ot.size());
		for (int i=0;i&amp;lt;ot.size();i++) a[i] -= ot.a[i];
		normalize();
		return *this;
	}
	Poly&amp;amp; operator *= (const Poly&amp;amp; ot){
		*this = fft_t::convolution(a, ot.a);
		normalize();
		return *this;
	}
	Poly&amp;amp; operator /= (const Poly&amp;amp; ot){
		if (deg() &amp;lt; ot.deg()) return *this = Poly();
		int sz = deg()-ot.deg()+1;
		Poly ra = reversed().trim(sz), rb = ot.reversed().trim(sz).inv(sz);
		*this = (ra*rb).trim(sz);
		while (size() &amp;lt; sz) emplace_back();
		reverse(a.begin(), a.end());
		normalize();
		return *this;
	}
	Poly&amp;amp; operator %= (const Poly&amp;amp; ot){
		if (deg() &amp;lt; ot.deg()) return *this;
		Poly tmp = *this; tmp /= ot; tmp *= ot;
		*this -= tmp;
		normalize();
		return *this;
	}

	Poly operator * (const T&amp;amp; x)const{ return Poly(*this) *= x; }
	Poly operator / (const T&amp;amp; x)const{ return Poly(*this) /= x; }
	Poly operator + (const Poly&amp;amp; ot)const{ return Poly(*this) += ot; }
	Poly operator - (const Poly&amp;amp; ot)const{ return Poly(*this) -= ot; }
	Poly operator * (const Poly&amp;amp; ot)const{ return Poly(*this) *= ot; }
	Poly operator / (const Poly&amp;amp; ot)const{ return Poly(*this) /= ot; }
	Poly operator % (const Poly&amp;amp; ot)const{ return Poly(*this) %= ot; }
};

constexpr int MOD = 104857601, PR = 3; // (25 &amp;lt;&amp;lt; 22) + 1
using mint_t = MINT&amp;lt;MOD&amp;gt;;
using poly_t = Poly&amp;lt;MOD, PR&amp;gt;;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int K; lld N; cin &amp;gt;&amp;gt; K &amp;gt;&amp;gt; N; N--;
	vector&amp;lt;mint_t&amp;gt; A(K), B(K+1);
	for (auto&amp;amp; v: A) cin &amp;gt;&amp;gt; v;
	for (int i=1;i&amp;lt;=K;i++) cin &amp;gt;&amp;gt; B[i], B[i] *= -1;
	B[0] = 1;
	poly_t F(A), Q(B);
	poly_t P = (F*Q).trim(K);
	for (;N;N&amp;gt;&amp;gt;=1){
		poly_t Q2 = Q;
		for (int i=1;i&amp;lt;Q2.size();i+=2) Q2[i] *= -1;
		poly_t U = P*Q2, V = Q*Q2;
		P.resize(K); U.resize(K&amp;lt;&amp;lt;1); V.resize(K&amp;lt;&amp;lt;1|1); // prevent out of index
		for (int i=N&amp;amp;1;i&amp;lt;(K&amp;lt;&amp;lt;1);i+=2) P[i&amp;gt;&amp;gt;1] = U[i];
		Q.resize(K+1);
		for (int i=0;i&amp;lt;=K;i++) Q[i] = V[i&amp;lt;&amp;lt;1];
	}
	cout &amp;lt;&amp;lt; P[0]/Q[0] &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>공부</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/149</guid>
      <comments>https://mwchun.tistory.com/149#entry149comment</comments>
      <pubDate>Wed, 30 Nov 2022 15:53:42 +0900</pubDate>
    </item>
    <item>
      <title>선형점화식 빠르게 계산하기</title>
      <link>https://mwchun.tistory.com/148</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;$D_0, D_1, \cdots, D_{k-1}$과 $c_1, c_2, \cdots, c_k$가 주어졌을 때, $i \ge k$인 $D_i$를 다음과 같은 선형점화식으로 구할 수 있다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$D_i = \sum_{j=1}^{k}{c_jD_{i-j}} = c_1D_{i-1} + c_2D_{i-2} + \cdots + c_kD_{i-k}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 선형점화식을 가지는 가장 유명한 예시는 피보나치 수열이다. 피보나치 수열은 위 식에서 $k=2$, $D_0 = 1$, $D_1 = 1$, $c_1 = 1$, $c_2 = 2$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 선형점화식이 주어졌을 때, $D_N$을 빠르게 구하는 방법을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 행렬 곱셈을 이용한 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\begin{pmatrix}&lt;br /&gt;D_N \\&lt;br /&gt;D_{N-1}\\&lt;br /&gt;D_{N-2}\\&lt;br /&gt;\vdots \\&lt;br /&gt;D_{N-k+1}&lt;br /&gt;\end{pmatrix}&lt;br /&gt;=&lt;br /&gt;\begin{pmatrix}&lt;br /&gt;c_1 &amp;amp; c_2 &amp;amp; \cdots &amp;amp; c_{k-1} &amp;amp; c_{k} \\&lt;br /&gt;1 &amp;amp; 0 &amp;amp; \cdots &amp;amp; 0 &amp;amp; 0 \\&lt;br /&gt;0 &amp;amp; 1 &amp;amp; \cdots &amp;amp; 0 &amp;amp; 0\\&lt;br /&gt;\vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots &amp;amp; \vdots&amp;nbsp; \\&lt;br /&gt;0 &amp;amp; 0 &amp;amp; \cdots &amp;amp; 1 &amp;amp; 0&lt;br /&gt;\end{pmatrix}&lt;br /&gt;\begin{pmatrix}&lt;br /&gt;D_{N-1} \\ D_{N-2} \\ D_{N-3} \\ \vdots \\ D_{N-k}&lt;br /&gt;\end{pmatrix}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 선형점화식을 위와 같이 행렬식으로 표현할 수 있다. 이 $k \times k$ 크기의 행렬을 $A$라고 하자. 이는 다음과 같이 거듭제곱을 이용하여 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\begin{pmatrix}&lt;br /&gt;D_N \\&lt;br /&gt;D_{N-1}\\&lt;br /&gt;D_{N-2}\\&lt;br /&gt;\vdots \\&lt;br /&gt;D_{N-k+1}&lt;br /&gt;\end{pmatrix}&lt;br /&gt;=&lt;br /&gt;\begin{pmatrix}&lt;br /&gt;c_1&amp;nbsp;&amp;amp;&amp;nbsp;c_2&amp;nbsp;&amp;amp;&amp;nbsp;\cdots&amp;nbsp;&amp;amp;&amp;nbsp;c_{k-1}&amp;nbsp;&amp;amp;&amp;nbsp;c_{k}&amp;nbsp;\\&lt;br /&gt;1&amp;nbsp;&amp;amp;&amp;nbsp;0&amp;nbsp;&amp;amp;&amp;nbsp;\cdots&amp;nbsp;&amp;amp;&amp;nbsp;0&amp;nbsp;&amp;amp;&amp;nbsp;0&amp;nbsp;\\&lt;br /&gt;0&amp;nbsp;&amp;amp;&amp;nbsp;1&amp;nbsp;&amp;amp;&amp;nbsp;\cdots&amp;nbsp;&amp;amp;&amp;nbsp;0&amp;nbsp;&amp;amp;&amp;nbsp;0\\&lt;br /&gt;\vdots&amp;nbsp;&amp;amp;&amp;nbsp;\vdots&amp;nbsp;&amp;amp;&amp;nbsp;\ddots&amp;nbsp;&amp;amp;&amp;nbsp;\vdots&amp;nbsp;&amp;amp;&amp;nbsp;\vdots&amp;nbsp;&amp;nbsp;\\&lt;br /&gt;0&amp;nbsp;&amp;amp;&amp;nbsp;0&amp;nbsp;&amp;amp;&amp;nbsp;\cdots&amp;nbsp;&amp;amp;&amp;nbsp;1&amp;nbsp;&amp;amp;&amp;nbsp;0&lt;br /&gt;\end{pmatrix}^{N-k+1}&lt;br /&gt;\begin{pmatrix}&lt;br /&gt;D_{k-1} \\ D_{k-2} \\ D_{k-3} \\ \vdots \\ D_{0}&lt;br /&gt;\end{pmatrix} = A^{N-k+1}&lt;br /&gt;\begin{pmatrix}&lt;br /&gt;D_{k-1} \\ D_{k-2} \\ D_{k-3} \\ \vdots \\ D_{0}&lt;br /&gt;\end{pmatrix}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 정수 $a$의 $a^N$을 분할정복을 통해 $O(\lg N)$ 시간복잡도에 구할 수 있다. 이와 같은 방법으로 크기 $k \times k$인 행렬 $A$의 $A^N$을 $O(k^3 \lg N)$ 시간복잡도에 구할 수 있다. 이렇게 $A^{N-k+1}$을 구하면 $D_N$을 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 키타마사법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 $k$가 크다면 $O(k^3 \lg n)$ 시간복잡도는 느릴 것이다. 좀 더 빠르게 구하는 방법을 예시를 통해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$k=2$이고, 점화식은 $D_i = 2D_{i-1}+3D_{i-2}$이다. $D_5$를 구하는 과정을 살펴보자. 이제 $D_5 = aD_1 + bD_0$ 꼴로 표현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$D_5 = 2D_4+3D_3 = 2(2D_3+3D_2)+3D_3\\ = 7D_3+6D_2 = 7(2D_2+3D_1)+6D_2\\ = 20D_2 + 21D_1 = 20(2D_1+3D_0)+21D_1\\ = 61D_1+60D_0$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$D_1$과 $D_0$을 알고 있기 때문에 $D_5$를 바로 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점화식을 다항식으로 표현해보자. $x^i = 2x^{i-1}+3x^{i-2}$이며, $x^2 = 2x + 3$, 즉, $x^2-2x-3 = 0$이다. 다항식 $x^N$를 $x^2-2x-3$으로 나눠보면 다음과 같이 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$x^N = Q(x^2-2x-3) + R$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다항식 $Q$는 나눗셈의 몫이 되며, 다항식 $R$은 나머지가 된다. 그리고 다항식 $R$의 차수는 2보다 작다. 만약, 다항식 $x^5$을 $x^2-2x-3$으로 나눠보면 다음과 같이 표현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$x^5 = (x^3+2x^2+7x+20)(x^2-2x-3) + 61x + 60$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 다항식 $R$의 계수가 $D_N = aD_1 + bD_0$ 꼴에서 $a$와 $b$가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반화하면 점화식을 $g(x) = x^k - c_1x^{k-1} - \cdots - c_{k-1}x - c_k = 0$으로 나타내고 $x^N$을 $g(x)$로 나눈 나머지를 구하면 $D_N$을 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$x^N =&amp;nbsp;&lt;br /&gt;\begin{cases}&lt;br /&gt;x^{\lfloor\frac{N}{2}\rfloor}x^{\lfloor\frac{N}{2}\rfloor}x &amp;amp; \mod g(x), &amp;amp; \text{if } N \text{ is odd}\\&lt;br /&gt;x^{\frac{N}{2}}x^{\frac{N}{2}} &amp;amp; \mod g(x), &amp;amp; \text{otherwise}&lt;br /&gt;\end{cases}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다항식의 경우에도 위 식이 성립하므로 $x^N$을 $g(x)$로 나눈 나머지를 $O(\lg N)$번의 곱셈과 나눗셈으로 구할 수 있다. 매 순간 $g(x)$로 나눈 나머지만 가지고 있으면 되므로, 곱하고 나누는 다항식의 차수는 $k$ 보다 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차수가 $k$인 다항식의 곱셈과 나눗셈을 $O(k^2)$ 시간복잡도에 할 수 있는데, 이 경우 전체 시간복잡도는 $O(k^2 \lg N)$이 된다. 1번 방법과 비교했을 때 $k$ 배 빠른 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 키타마사법 + NTT + 빠른 다항식 나눗셈&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 방법에서 다항식의 곱셈과 다항식의 나눗셈에 $O(k^2)$ 시간이 필요했으므로 전체 시간복잡도가 $O(k^2 \lg N)$이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 문제의 MOD 값이 $a2^b+1$ 꼴의 소수이며 원시근을 알고 $k &amp;lt; 2^b$라면 NTT가 가능하다. 다항식의 곱셈은 NTT 그 자체로 $O(k \lg k)$ 시간에 가능하며, 나눗셈 또한 &lt;a href=&quot;https://blog.myungwoo.kr/147&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글&lt;/a&gt;에 있는 방법을 통해 $O(k \lg k)$에 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 시간복잡도는 $O(k \lg k \lg N)$이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) Bostan Mori 알고리즘&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bostan Mori 알고리즘에 대한 자세한 설명은 &lt;a href=&quot;https://blog.myungwoo.kr/149&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글&lt;/a&gt;을 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 알고리즘 또한 NTT를 필요로 하므로 3번 방법과 사용 가능한 조건이 동일하다. 시간복잡도 또한 $O(k \lg k \lg N)$으로 3번 방법과 동일하다. 그러나 다항식의 나눗셈을 필요로 하지 않는다는 장점이 있다. 다항식의 나눗셈의 경우, 시간복잡도는 $O(k \lg k)$이지만, 상수가 크고 별도로 구현해야 한다는 번거로움이 있기 때문에 이 알고리즘은 구현이 상대적으로 편하며 실행 시간이 3번 방법보다 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*) 예시 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 방법의 기본 연습 문제다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1669769478131&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2749번: 피보나치 수 3&quot; data-og-description=&quot;첫째 줄에 n이 주어진다. n은 1,000,000,000,000,000,000보다 작거나 같은 자연수이다.&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://boj.kr/2749&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2749&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OE5FQ/hyQK16VB7o/MzInR57yhcfkgCKctJYVQk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://boj.kr/2749&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://boj.kr/2749&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OE5FQ/hyQK16VB7o/MzInR57yhcfkgCKctJYVQk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2749번: 피보나치 수 3&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 n이 주어진다. n은 1,000,000,000,000,000,000보다 작거나 같은 자연수이다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MOD 값이 $1999 = 999\times2^1+1$이기 때문에 NTT를 쓸 수 없다. 2번 방법, $O(k^2 \lg N)$으로 구현해야 한다.&lt;/p&gt;
&lt;figure id=&quot;og_1669769567026&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;15572번: 블록 4&quot; data-og-description=&quot;여러 가지 블록들을 이용하여 직사각형 모양을 만들려고 한다. 우리에게는&amp;nbsp;1&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록,&amp;nbsp;2&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록, ...,&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록이 무한하게 있다. 이 블록들을 사용하여&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;M&amp;nbsp;모양을 만들고 &quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://boj.kr/15572&quot; data-og-url=&quot;https://www.acmicpc.net/problem/15572&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bXjQVj/hyQJAXnYOB/kwhsNZ1htBkIY2lnQva7Ik/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://boj.kr/15572&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://boj.kr/15572&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bXjQVj/hyQJAXnYOB/kwhsNZ1htBkIY2lnQva7Ik/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;15572번: 블록 4&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;여러 가지 블록들을 이용하여 직사각형 모양을 만들려고 한다. 우리에게는&amp;nbsp;1&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록,&amp;nbsp;2&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록, ...,&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;N&amp;nbsp;블록이 무한하게 있다. 이 블록들을 사용하여&amp;nbsp;N&amp;thinsp;&amp;times;&amp;thinsp;M&amp;nbsp;모양을 만들고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MOD 값이 $104857601 = 25 \times 2^{22}+1$ 로 NTT를 쓸 수 있다. 원시근은 2부터 차례대로 구해보면 되는데, 3이 원시근이 된다. $k$의 최댓값이 30000으로 크기 때문에 1번, 2번 방법으로 풀 수 없다.&lt;/p&gt;
&lt;figure id=&quot;og_1669769422527&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;13725번: RNG&quot; data-og-description=&quot;첫째 줄에 k와 N (1 &amp;le; k &amp;le; 30,000, 1 &amp;le; N &amp;le; 1018)이 주어진다. 둘째 줄에는 A1, A2, ..., Ak가 셋째 줄에는 C1, C2, ..., Ck가 주어진다. (0 &amp;le; Ai, Ci &amp;lt; 104857601)&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://boj.kr/13725&quot; data-og-url=&quot;https://www.acmicpc.net/problem/13725&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBRPcL/hyQJA4chmU/Dnyz5lOG9CmvrlhoJdt5m1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://boj.kr/13725&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://boj.kr/13725&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBRPcL/hyQJA4chmU/Dnyz5lOG9CmvrlhoJdt5m1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;13725번: RNG&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 k와 N (1 &amp;le; k &amp;le; 30,000, 1 &amp;le; N &amp;le; 1018)이 주어진다. 둘째 줄에는 A1, A2, ..., Ak가 셋째 줄에는 C1, C2, ..., Ck가 주어진다. (0 &amp;le; Ai, Ci &amp;lt; 104857601)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 BOJ 13725 RNG 문제를 해결하는 코드이며, mod int와 NTT를 이용한 다항식 템플릿을 포함한 코드다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

template &amp;lt;int MOD&amp;gt;
struct MINT{
	int v{};
	MINT() = default;
	constexpr MINT(lld v_){
		v = -MOD &amp;lt;= v_ &amp;amp;&amp;amp; v_ &amp;lt; MOD ? v_ : v_ % MOD;
		if (v &amp;lt; 0) v += MOD;
	}

	friend istream&amp;amp; operator &amp;gt;&amp;gt; (istream&amp;amp; is, MINT&amp;amp; a){ lld t; is &amp;gt;&amp;gt; t; a = MINT(t); return is; }
	friend ostream&amp;amp; operator &amp;lt;&amp;lt; (ostream&amp;amp; os, const MINT&amp;amp; a){ return os &amp;lt;&amp;lt; a.v; }
	bool operator == (const MINT&amp;amp; ot)const{ return v == ot.v; }
	bool operator != (const MINT&amp;amp; ot)const{ return v != ot.v; }
	static constexpr MINT pow(MINT a, lld b){
		MINT ret = 1;
		for (;b;b&amp;gt;&amp;gt;=1,a*=a) if (b&amp;amp;1) ret *= a;
		return ret;
	}
	static constexpr MINT inv(const MINT&amp;amp; t){ return pow(t, MOD-2); }
	constexpr MINT operator - ()const{ return MINT(-v); }
	constexpr MINT&amp;amp; operator += (const MINT&amp;amp; ot){ if ((v += ot.v) &amp;gt;= MOD) v -= MOD; return *this; }
	constexpr MINT&amp;amp; operator -= (const MINT&amp;amp; ot){ if ((v -= ot.v) &amp;lt; 0) v += MOD; return *this; }
	constexpr MINT&amp;amp; operator *= (const MINT&amp;amp; ot){ v = (lld)v*ot.v%MOD; return *this; }
	constexpr MINT&amp;amp; operator /= (const MINT&amp;amp; ot){ *this *= inv(ot); return *this; }
	constexpr MINT operator + (const MINT&amp;amp; ot)const{ return MINT(*this) += ot; }
	constexpr MINT operator - (const MINT&amp;amp; ot)const{ return MINT(*this) -= ot; }
	constexpr MINT operator * (const MINT&amp;amp; ot)const{ return MINT(*this) *= ot; }
	constexpr MINT operator / (const MINT&amp;amp; ot)const{ return MINT(*this) /= ot; }
	constexpr operator int32_t ()const{ return v; }
	constexpr operator int64_t ()const{ return v; }
};

template &amp;lt;int prime = (119&amp;lt;&amp;lt;23)+1, int primitive_root = 3&amp;gt;
struct FFT{
	using T = MINT&amp;lt;prime&amp;gt;;
	static constexpr int P = prime;
	static constexpr int PR = primitive_root;
	static constexpr int PRI = T::inv(PR);
	static constexpr int MSZ = (P - 1) &amp;amp; (1 - P);
	static constexpr int W = T::pow(PR, (P - 1) / MSZ);
	static constexpr int WI = T::pow(PRI, (P - 1) / MSZ);

	static void fft(int sz, vector&amp;lt;T&amp;gt;&amp;amp; a, bool inv){
		T w = T::pow(inv ? WI : W, MSZ / sz);
		for (int ssz=sz&amp;gt;&amp;gt;1;ssz;ssz&amp;gt;&amp;gt;=1){
			for (int i=0;i&amp;lt;sz;i+=ssz&amp;lt;&amp;lt;1){
				T wt = 1;
				for (int j=0;j&amp;lt;ssz;++j){
					T&amp;amp; lft = a[i + j];
					T&amp;amp; rgh = a[i + j + ssz];
					T lftold = lft;
					lft += rgh;
					rgh = (lftold - rgh) * wt;
					wt *= w;
				}
			}
			w *= w;
		}
		if (inv){
			T invsz = T::inv(sz);
			for (int i=0;i&amp;lt;sz;i++) a[i] *= invsz;
		}
		for (int j=0,i=1;i&amp;lt;sz;i++){
			int dg = sz&amp;gt;&amp;gt;1;
			for (;j&amp;amp;dg;dg&amp;gt;&amp;gt;=1) j ^= dg;
			j ^= dg;
			if (i &amp;lt; j) swap(a[i], a[j]);
		}
	}

	static vector&amp;lt;T&amp;gt; convolution(const vector&amp;lt;T&amp;gt;&amp;amp; a, const vector&amp;lt;T&amp;gt;&amp;amp; b){
		int sz = a.size() + b.size();
		int fftsz = sz;
		while (fftsz &amp;amp; (fftsz - 1)) fftsz += fftsz &amp;amp; -fftsz;
		vector&amp;lt;T&amp;gt; av(fftsz);
		vector&amp;lt;T&amp;gt; bv(fftsz);
		for (int i=0,ilim=size(a);i&amp;lt;ilim;i++) av[i] = a[i];
		for (int i=0,ilim=size(b);i&amp;lt;ilim;i++) bv[i] = b[i];
		fft(fftsz, av, false);
		fft(fftsz, bv, false);
		for (int i=0;i&amp;lt;fftsz;i++) av[i] *= bv[i];
		fft(fftsz, av, true);
		return av;
	}
};

// alternatives
using FFT1 = FFT&amp;lt;1'012'924'417, 5&amp;gt;; // (483&amp;lt;&amp;lt;21)+1
using FFT2 = FFT&amp;lt;1'004'535'809, 3&amp;gt;; // (479&amp;lt;&amp;lt;21)+1

template &amp;lt;int prime = (119&amp;lt;&amp;lt;23)+1, int primitive_root = 3&amp;gt;
struct Poly{
	using T = MINT&amp;lt;prime&amp;gt;;
	using fft_t = FFT&amp;lt;prime, primitive_root&amp;gt;;
	vector&amp;lt;T&amp;gt; a;
	Poly() = default;
	Poly(T a0): a(1, a0){ normalize(); }
	Poly(const vector&amp;lt;T&amp;gt;&amp;amp; a): a(a) { normalize(); }

	int size()const{ return a.size(); }
	int deg()const{ return a.size()-1; }
	T&amp;amp; operator [](int i){ return a[i]; }
	typename vector&amp;lt;T&amp;gt;::const_iterator begin()const{ return a.begin(); }
	typename vector&amp;lt;T&amp;gt;::const_iterator end()const{ return a.end(); }
	void push_back(const T&amp;amp; v){ a.push_back(v); }
	template&amp;lt;typename... Args&amp;gt; void emplace_back(Args&amp;amp;&amp;amp;... args){ a.emplace_back(args...); }
	void pop_back(){ a.pop_back(); }
	void resize(int n){ a.resize(n); }

	void normalize(){ while (!a.empty() &amp;amp;&amp;amp; a.back() == T(0)) a.pop_back(); }

	Poly reversed()const{ auto b = a; reverse(b.begin(), b.end()); return b; }
	Poly trim(int n)const{ return vector&amp;lt;T&amp;gt;(a.begin(), a.begin() + min(n, size())); }
	Poly inv(int n){
		Poly q(T(1) / a[0]);
		for (int i=1;i&amp;lt;n;i&amp;lt;&amp;lt;=1){
			Poly p = Poly(2) - q*trim(i&amp;lt;&amp;lt;1);
			q = (p * q).trim(i&amp;lt;&amp;lt;1);
		}
		return q.trim(n);
	}

	Poly&amp;amp; operator *= (const T&amp;amp; x){ for (T&amp;amp; v: a) v *= x; normalize(); return *this; }
	Poly&amp;amp; operator /= (const T&amp;amp; x){ return *this *= T(1) / x; }
	Poly&amp;amp; operator += (const Poly&amp;amp; ot){
		if (size() &amp;lt; ot.size()) a.resize(ot.size());
		for (int i=0;i&amp;lt;ot.size();i++) a[i] += ot.a[i];
		normalize();
		return *this;
	}
	Poly&amp;amp; operator -= (const Poly&amp;amp; ot){
		if (size() &amp;lt; ot.size()) a.resize(ot.size());
		for (int i=0;i&amp;lt;ot.size();i++) a[i] -= ot.a[i];
		normalize();
		return *this;
	}
	Poly&amp;amp; operator *= (const Poly&amp;amp; ot){
		*this = fft_t::convolution(a, ot.a);
		normalize();
		return *this;
	}
	Poly&amp;amp; operator /= (const Poly&amp;amp; ot){
		if (deg() &amp;lt; ot.deg()) return *this = Poly();
		int sz = deg()-ot.deg()+1;
		Poly ra = reversed().trim(sz), rb = ot.reversed().trim(sz).inv(sz);
		*this = (ra*rb).trim(sz);
		while (size() &amp;lt; sz) emplace_back();
		reverse(a.begin(), a.end());
		normalize();
		return *this;
	}
	Poly&amp;amp; operator %= (const Poly&amp;amp; ot){
		if (deg() &amp;lt; ot.deg()) return *this;
		Poly tmp = *this; tmp /= ot; tmp *= ot;
		*this -= tmp;
		normalize();
		return *this;
	}

	Poly operator * (const T&amp;amp; x)const{ return Poly(*this) *= x; }
	Poly operator / (const T&amp;amp; x)const{ return Poly(*this) /= x; }
	Poly operator + (const Poly&amp;amp; ot)const{ return Poly(*this) += ot; }
	Poly operator - (const Poly&amp;amp; ot)const{ return Poly(*this) -= ot; }
	Poly operator * (const Poly&amp;amp; ot)const{ return Poly(*this) *= ot; }
	Poly operator / (const Poly&amp;amp; ot)const{ return Poly(*this) /= ot; }
	Poly operator % (const Poly&amp;amp; ot)const{ return Poly(*this) %= ot; }
};

constexpr int MOD = 104857601, PR = 3; // (25 &amp;lt;&amp;lt; 22) + 1
using mint_t = MINT&amp;lt;MOD&amp;gt;;
using poly_t = Poly&amp;lt;MOD, PR&amp;gt;;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int K; lld N; cin &amp;gt;&amp;gt; K &amp;gt;&amp;gt; N;
	vector&amp;lt;mint_t&amp;gt; A(K), B(K);
	for (auto&amp;amp; v: A) cin &amp;gt;&amp;gt; v;
	for (auto&amp;amp; v: B) cin &amp;gt;&amp;gt; v, v *= -1;
	reverse(B.begin(), B.end());
	B.emplace_back(1);
	N--;
	if (N &amp;lt; K) return cout &amp;lt;&amp;lt; A[N] &amp;lt;&amp;lt; '\n', 0;
	poly_t res(1), v({0, 1});
	for (int i=0;i&amp;lt;60;i++,v=v*v%B) if (N&amp;gt;&amp;gt;i&amp;amp;1) res = res*v%B;
	mint_t ans = 0;
	for (int i=0;i&amp;lt;K;i++) ans += res[i]*A[i];
	cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>공부</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/148</guid>
      <comments>https://mwchun.tistory.com/148#entry148comment</comments>
      <pubDate>Wed, 30 Nov 2022 10:14:46 +0900</pubDate>
    </item>
    <item>
      <title>다항식 나눗셈의 몫을 빠르게 구하는 방법</title>
      <link>https://mwchun.tistory.com/147</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;차수가 $n$인 다항식 $f(x) = c_0 + c_1x^1 + c_2x^2 + \cdots + c_nx^n (c_n \ne 0)$가 있다. 그리고 차수 $m$인 다항식 $g(x) = d_0 + d_1x^1 + d_2x^2 + \cdots + d_mx^m (d_m \ne 0)$이 있다. 이를 다항식 $q$와 $r$을 이용하여, $f(x) = g(x)q(x) + r(x)$라고 나타내 보자. $r(x)$의 차수가 $m$보다 작은 경우, $f$를 $g$로 나눈 몫을 $q$, 나머지를 $r$이라고 정의한다. 다항식 $q$의 차수는 $n-m$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다항식의 나눗셈을 교과과정에서 배운대로 구현한다면 $O(nm)$ 시간복잡도가 된다. 이를 좀 더 빠르게 하는 방법에 대해서 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$\textrm{Rev}(f)$를 다항식 $f$의 계수를 좌우로 뒤집은 것으로 정의하자. 즉, $\textrm{Rev}(f) = c_n + c_{n-1}x^1 + c_{n-2}x^2 + \cdots + c_0x^n$이다. 이는 다음과 같은 성질을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\textrm{Rev}(f) = x^nf(\frac{1}{x}) \Rightarrow \textrm{Rev}(fg) = \textrm{Rev}(f)\textrm{Rev}(g)$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$f = gq + r$이라는 식에 Rev를 씌워보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\textrm{Rev}(f) = \textrm{Rev}(g)\textrm{Rev}(q) + x^{n-\deg(r)}\textrm{Rev}(r)$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, $\deg(r)$은 다항식 $r$의 차수를 의미한다. $\deg(r) &amp;lt; m$이므로 $n-\deg(r) \ge n-m+1$이다. 따라서 아래 식을 만족한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\textrm{Rev}(f) = \textrm{Rev}(g)\textrm{Rev}(q) \mod x^{n-m+1}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, $\textrm{Rev}(g)$의 modulo에서의 역함수 $\textrm{Rev}(g)^{-1}$이 존재한다면 다음 식을 만족한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\textrm{Rev}(q) = \textrm{Rev}(f)\textrm{Rev}(g)^{-1} \mod x^{n-m+1}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다항식 $q$의 차수가 $n-m$이기 때문에 $\textrm{Rev}(q) \mod x^{n-m+1}$는 $\textrm{Rev}(q)$가 되고, 이를 통해 다항식 $q$를 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몫이 되는 다항식 $q$를 구하기 위해, $\textrm{Rev}(g)^{-1} \mod x^{n-m+1}$을 구하는 방법을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 일반화하여, 상수 계수가 0이 아닌 다항식 $h$에 대해 $h^{-1} \textrm{ mod } x^l$을 구하는 방법을 알아보자. 다항식 $h$의 상수 계수를 $t$라고 한다면, $l = 1$일 때 $h^{-1} = \frac{1}{t} \textrm{ mod } x$가 된다. 이제 $h^{-1} \textrm{ mod } x^l$을 알고 있을 때, $h^{-1} \textrm{ mod } x^{2l}$을 구하는 귀납적인 방법으로 $l = 2^k$ 꼴일 때 $h^{-1} \textrm{ mod } x^l$을 구할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ah = 1 \textrm{ mod } x^l$인 다항식 $a$가 있다. 우리가 구하고 싶은 것은 $\tilde{a}h = 1 \textrm{ mod } x^{2l}$인 $\tilde{a}$이다. $\tilde{a} = a + bx^l$를 만족하는 다항식 $b$가 존재한다. 또한, $h = h_0 + h_1x^l$라고 표현할 수 있다. 여기서 다항식 $a$와 다항식 $h_0$의 차수는 $l$ 미만이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\tilde{a}h = (a+bx^l)(h_0+h_1x^l) = ah_0 + (bh_0 + ah_1)x^l = 1 \mod x^{2l}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ah_0 = 1 + cx^l \textrm{ mod } x^{2l}$으로 나타낼 수 있다. 이것을 위 식에 대입하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\tilde{a}h = 1 + (c+bh_0+ah_1)x^l = 1 \mod x^{2l}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, $c+bh_0+ah_1 = 0 \textrm{ mod } x^{2l}$이 됨을 알 수 있다. 이를 $b$에 대한 식으로 표현하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$b = -h_0^{-1}(ah_1+c) = -a(ah_1+c) \mod x^{2l}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 $\tilde{a} = a+bx^l$에 대입하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$\tilde{a} = a + (-a^2h_1-ac)x^l \\= a \left[ 1-cx^l-ah_1x^l \right] \\= a \left[ 2 - a(h_0+h_1x^l) \right] \\= a \left[ 2 - ah \right] \mod x^{2l}$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 2번의 다항식 곱셈과 1번의 다항식 덧셈을 하면 $l$이 2배 증가한다. $l \ge n-m+1$이 될 때까지 반복하여 $\textrm{Rev}(g)^{-1}$을 구하고, 이를 $\textrm{Rev}(f)$에 곱하면 $\textrm{Rev}(q)$를 구할 수 있다. 이제 다항식 계수의 순서만 좌우로 다시 뒤집으면 구하고자 하는 몫이 되는 다항식 $q$를 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다항식의 곱은 &lt;a href=&quot;https://blog.myungwoo.kr/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FFT&lt;/a&gt;를 이용하여 $O(n \lg n)$ 시간복잡도에 수행할 수 있다. 곱하는 다항식의 차수는 처음 1에서 시작하여 두 배씩 증가하고 $n-m+1$과 같거나 커질 때까지 진행된다. 전체 시간복잡도를 계산해보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$O(1 \lg 1 + 2 \lg 2 + 4 \lg 4 + \cdots + n \lg n) \le k(1+2+4+\cdots+n) \lg n \le 2k n \lg n = O(n \lg n)$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 특정한 조건 안에서 키타마사법과 NTT를 섞어 사용하여 선형점화식을 가지는 수열의 $N$ 번째 항을 $O(k \lg k \lg N)$ 시간안에 구할 때 유용하게 사용된다. &lt;a href=&quot;https://blog.myungwoo.kr/148&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 글&lt;/a&gt;을 참고하자.&lt;/p&gt;</description>
      <category>공부</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/147</guid>
      <comments>https://mwchun.tistory.com/147#entry147comment</comments>
      <pubDate>Tue, 29 Nov 2022 23:09:38 +0900</pubDate>
    </item>
    <item>
      <title>Nexon Youth Programming Challenge(NYPC) 2022 본선 풀이</title>
      <link>https://mwchun.tistory.com/146</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1214 A. 조약돌 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 $[1, 2, 3, ..., K]$이 적혀있는 크기 $K$인 배열 $A$가 있다. $i = 1$부터 시작하여 $A_i$와 $A_{i+1}$의 값을 바꾼다. 그리고 $i$를 $1$ 증가시킨다. 만약 $i = K-1$이라면 다음 $i$의 값은 $K$가 아니라, $1$이 된다. 바꾸는 작업을 $N$ 번 하였을 때, 최종 배열의 상태를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 바꾸는 횟수 $N$이 최대 $10^{18}$로 굉장히 크다. 바꾸는 작업을 $K \times (K-1)$ 번 하면 배열이 원래 상태로 돌아오고, 바꾸는 작업을 $K-1$ 번하면 맨 왼쪽에 있던 값이 맨 오른쪽으로 가는 것을 알 수 있다. 이 점을 이용하여, $O(K)$ 시간복잡도에 해결할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int K; lld N;
	cin &amp;gt;&amp;gt; K &amp;gt;&amp;gt; N; N %= (lld)K*(K-1);
	int A[K+1];
	for (int i=1;i&amp;lt;=K;i++) A[i] = i;
	int S = N/(K-1); N %= K-1;
	rotate(A+1, A+1+S, A+K+1);
	int i = 1;
	while (N--){
		swap(A[i], A[i+1]);
		i = i+1 &amp;lt; K ? i+1 : 1;
	}
	for (int i=1;i&amp;lt;=K;i++) cout &amp;lt;&amp;lt; A[i] &amp;lt;&amp;lt; ' ';
	cout &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1214 B. 짝 맞는 문자열&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A, B, C로 이루어진 길이 $N$인 문자열이 주어진다. 이 문자열에서 최소 개수의 문자를 제거하여 같은 문자가 두 개씩 반복되게 하는 문제다. 즉, 제거할 문자를 모두 제거한 후, $1$ 이상인 $i$에 대해 $2i-1$ 번째 문자와 $2i$ 번째 문자가 같아야 하며, 이 둘이 같으면 짝이 맞는다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D[i][j] = 문자열의 길이 $i$인 접두사에서 상태가 $j$일 때 제거하고 남은 문자의 최대 개수라고 정의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 $j$는 만약 모든 문자가 짝이 맞는다면 $j = 0$, 맨 마지막 문자를 제외한 모든 문자가 짝이 맞는다면 $j$는 맨 마지막 문자로 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 요구하는 답은 D[N][0]이 되며, 이 DP 배열은 시간복잡도 $O(N)$만에 구할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	string S; cin &amp;gt;&amp;gt; S;
	int N = size(S);
	int D[N+1][4];
	for (int i=0;i&amp;lt;=N;i++) for (int j=0;j&amp;lt;4;j++) D[i][j] = -1;
	auto put_max = [](auto&amp;amp; a, auto b){
		a = max(a, b);
	};
	D[0][0] = 0;
	for (int i=0;i&amp;lt;N;i++){
		for (int j=0;j&amp;lt;4;j++) put_max(D[i+1][j], D[i][j]);
		if (D[i][0] &amp;gt;= 0) put_max(D[i+1][S[i]-'A'+1], D[i][0]+1);
		if (D[i][S[i]-'A'+1] &amp;gt;= 0) put_max(D[i+1][0], D[i][S[i]-'A'+1]+1);
	}
	cout &amp;lt;&amp;lt; D[N][0] &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1214 C / 1519 A. 빙고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N \times M$ 크기의 빙고판이 있다. 칸에 적힌 수는 알 수 없고, 사회자가 부르는 수도 알 수 없다. $i$ 번째 행을 완성하면 $A_i$점을 획득하며, $j$ 번째 열을 완성하면 $B_j$점을 획득한다. 사회자가 번호를 $K$ 개 부를 때, 얻을 수 있는 최대 점수를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$r$ 개의 행을 먼저 완성한다고 하자. 그러면 불러야 하는 수의 개수는 $r \times M$개다. 그 뒤로 열만 완성한다고 했을 때, 완성시킬 수 있는 열의 최대 개수는 $\lfloor\frac{K - r \times M}{N-1}\rfloor$로 정해진다. 획득할 수 있는 점수가 높은 행들과 열들을 우선적으로 완성하는 것이 제일 좋다. $0$ 이상 $N$ 미만인 모든 $r$에 대해 완성시킬 수 있는 열의 개수를 구하고, 점수를 계산하여, 그중 가장 높은 점수를 구하여 문제를 해결할 수 있다. 단, $K = N \times M$인 경우에 대한 예외 처리가 필요하다. 시간복잡도는 $O(N+M)$이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M; lld K; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M &amp;gt;&amp;gt; K;
	int A[N+1], B[M+1];
	for (int i=1;i&amp;lt;=N;i++) cin &amp;gt;&amp;gt; A[i];
	for (int i=1;i&amp;lt;=M;i++) cin &amp;gt;&amp;gt; B[i];
	sort(A+1, A+N+1, greater&amp;lt;int&amp;gt;());
	sort(B+1, B+M+1, greater&amp;lt;int&amp;gt;());

	lld P[N+1]{}, Q[M+1]{};
	for (int i=1;i&amp;lt;=N;i++) P[i] = P[i-1]+A[i];
	for (int i=1;i&amp;lt;=M;i++) Q[i] = Q[i-1]+B[i];

	if (K == (lld)N*M){
		lld ans = 0;
		for (int i=1;i&amp;lt;=N;i++) ans += A[i];
		for (int i=1;i&amp;lt;=M;i++) ans += B[i];
		cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
		return 0;
	}

	lld ans = 0;
	for (int rows=0;rows&amp;lt;N;rows++){
		if ((lld)M*rows &amp;gt; K) break;
		int cols = (K-(lld)M*rows) / (N-rows);
		ans = max(ans, P[rows]+Q[cols]);
	}
	cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1214 D / 1519 B. 야찌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$1$ 이상 $6$ 이하의 수가 적힌 $N$ 개의 카드가 있는 카드 더미가 있다. 그리고 5장의 종이가 있다. 각 턴마다 카드 더미 위에서 카드 5장을 뽑아 순서대로 종이에 적는다. 그리고 최대 한 번 수를 지울 종이를 고르고, 카드 더미에서 카드를 다시 뽑아 지운 종이 위에 수를 적을 수 있다. 만약, 종이 5장에 적힌 수가 모두 같다면  야찌다. 카드 더미에 대한 정보가 주어졌을 때, 가능한 야찌의 최대 횟수를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D[i] = 카드 더미에서 총 $i$ 장의 카드를 뽑았을 때, 할 수 있는 야찌의 최대 횟수라고 정의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 요구하는 답은 $\max(D_i)$이다. 한 턴에 뽑을 수 있는 카드의 최대 개수는 10장이므로, $O(N)$ 시간에 DP를 구현할 수 있다. 다만, $N$의 최대값이 $10^6$이므로, 구현에 따라 시간초과가 날 수도 있다. 아래 첨부된 코드는 비트마스크를 이용한 전처리 작업으로 최대한 상수를 줄였다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N; string S;
	cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; S;
	int A[N+1];
	for (int i=1;i&amp;lt;=N;i++) A[i] = S[i-1]-'0';
	bool can[11][1024]{};
	vector&amp;lt;int&amp;gt; info[11][1024];
	for (int i=5;i&amp;lt;=10;i++){
		for (int msk=0;msk&amp;lt;1&amp;lt;&amp;lt;i;msk++){
			auto&amp;amp; _info = info[i][msk];
			int arr[i];
			for (int j=0;j&amp;lt;i;j++) arr[j] = msk&amp;gt;&amp;gt;j&amp;amp;1;
			int cnt = 0;
			for (int j=0;j&amp;lt;5;j++) if (arr[j]) cnt++;
			int discard = i-5;
			for (int j=0;j&amp;lt;5;j++) if (!arr[j] &amp;amp;&amp;amp; size(_info) &amp;lt; discard) _info.push_back(j+1);
			for (int j=0;j&amp;lt;5;j++) if (arr[j] &amp;amp;&amp;amp; size(_info) &amp;lt; discard) _info.push_back(j+1);
			cnt = min(cnt, 5-discard);
			for (int j=5;j&amp;lt;i;j++) if (arr[j]) cnt++;
			if (cnt == 5) can[i][msk] = 1;
		}
	}
	vector&amp;lt;int&amp;gt; D(N+1, -1);
	pair&amp;lt;int, int&amp;gt; from[N+1];
	D[0] = 0;
	for (int i=0;i&amp;lt;N;i++) if (D[i] &amp;gt;= 0){
		for (int num=1;num&amp;lt;7;num++){
			int msk = 0;
			for (int j=1;j&amp;lt;=10;j++){
				if (i+j &amp;gt; N) break;
				if (A[i+j] == num) msk ^= 1&amp;lt;&amp;lt;j-1;
				if (j &amp;gt;= 5){
					if (D[i+j] &amp;lt; D[i]+can[j][msk]){
						D[i+j] = D[i]+can[j][msk];
						from[i+j] = make_pair(j, msk);
					}
				}
			}
		}
	}
	int ans = -1, t;
	for (int i=1;i&amp;lt;=N;i++) if (ans &amp;lt; D[i]){
		ans = D[i];
		t = i;
	}
	vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; path;
	while (t &amp;gt; 0){
		auto [len, msk] = from[t];
		path.push_back(info[len][msk]);
		t -= len;
	}
	reverse(begin(path), end(path));
	cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n' &amp;lt;&amp;lt; size(path) &amp;lt;&amp;lt; '\n';
	int turn = 0;
	for (auto&amp;amp; arr: path){
		cout &amp;lt;&amp;lt; &quot;Turn &quot; &amp;lt;&amp;lt; ++turn &amp;lt;&amp;lt; '\n';
		if (arr.empty()) cout &amp;lt;&amp;lt; &quot;0\n&quot;;
		else{
			cout &amp;lt;&amp;lt; &quot;1\n&quot; &amp;lt;&amp;lt; size(arr);
			for (int v: arr) cout &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; v;
			cout &amp;lt;&amp;lt; '\n';
		}
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1214 E. 삼각&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정점 $N$ 개와 양방향 간선 $M$ 개로 이루어진 그래프가 주어진다. 각 간선에는 길이가 있다. 정점 $S$가 주어진다. 정점 $S$에서 시작하여 어떤 정점 $A$로 이동했다가, 다른 정점 $B$로 이동하고, 다시 정점 $S$로 돌아오는 경로 중에 정점 $S$를 중간에 방문하지 않으면서 가장 길이가 짧은 경로를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제 풀이의 핵심 아이디어는 서로 인접한 정점 두 개를 정점 $A$와 정점 $B$만 답의 후보로 두어도 문제에서 요구하는 답을 올바르게 구한다는 점이다. 즉, 입력으로 주어진 정점 $S$에서 각 정점으로 가는 최단 거리를 구한 뒤, 정점 $a$와 정점 $b$를 연결하는 길이 $c$인 간선이 있다면, 정점 $S$에서 정점 $a$로 오는 최단 거리 값 + 정점 $S$에서 정점 $b$로 오는 최단 거리 값 + $c$를 구하고, 그중 가장 작은 값을 구하면 정답을 구할 수 있다. 시간복잡도는 $O(N + M \lg N)$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 풀이와 크게 상관 없는 관찰 중, 답이 되는 싸이클에 포함된 간선의 개수는 최대 $4$ 개라는 것도 증명할 수 있다. 이 점을 이용하여, 서브태스크를 해결할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

constexpr lld inf = 4e18;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M, S; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M &amp;gt;&amp;gt; S;
	vector&amp;lt;vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt; con(N+1);
	for (int i=1;i&amp;lt;=M;i++){
		int a, b, c; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
		con[a].emplace_back(b, c);
		con[b].emplace_back(a, c);
	}
	vector&amp;lt;lld&amp;gt; dist(N+1, inf);
	priority_queue&amp;lt;pair&amp;lt;lld, int&amp;gt;&amp;gt; pq;
	dist[S] = 0; pq.emplace(0, S);
	while (!pq.empty()){
		auto [d, n] = pq.top(); pq.pop();
		if (dist[n] != -d) continue;
		for (auto [t, v]: con[n]){
			if (dist[t] &amp;gt; dist[n]+v)
				dist[t] = dist[n]+v, pq.emplace(-dist[t], t);
		}
	}
	lld ans = inf;
	for (int i=1;i&amp;lt;=N;i++){
		for (auto [j, d]: con[i]){
			if (i == S || j == S) continue;
			ans = min(ans, dist[i]+dist[j]+d);
		}
	}
	cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1519 C. 덧셈 프로그램&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기 $N$인 정수 배열 $A$가 있고, $M$ 개의 명령어로 구성된 프로그램이 있다. 명령어는 $A_i$에 $A_j$를 더하는 꼴을 한다. 이 프로그램을 총 $L$ 번 실행하는데, 각 실행마다 배열의 초기값이 다를 수 있다. 각 실행마다 프로그램이 종료되는 시점에서 $A_1$의 값을 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기 $N$인 정수 배열 $B$를 생각하자. $B$의 초기값은 $B_1 = 1$이고, 나머지는 $0$이다. 배열 $B$에서 입력으로 주어진 프로그램의 명령어를 반대로 실행한다. 명령어의 순서도 반대가 되어야 하고, $A_i$에 $A_j$를 더하는 명령어가 있다면, $B_j$에 $B_i$를 더하자. 실행 이후의 $B_i$ 값은 주어진 프로그램에서 $A_i$의 초기값이 최종적으로 $A_1$에 몇번 더해졌는지를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하여 각 프로그램 실행마다, 프로그램이 종료되는 시점에서 $A_1$의 값을 빠르게 구할 수 있다. 초기값이 $0$이 아닌 $A_i$의 총개수를 $K$라고 했을 때, 시간복잡도는 $O(M+L+K)$이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

constexpr int MOD = 1e9 + 7;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M, L; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M &amp;gt;&amp;gt; L;
	int X[M+1], Y[M+1];
	for (int i=1;i&amp;lt;=M;i++) cin &amp;gt;&amp;gt; X[i] &amp;gt;&amp;gt; Y[i];
	int A[N+1]{};
	A[1] = 1;
	for (int i=M;i;i--){
		A[Y[i]] += A[X[i]];
		if (A[Y[i]] &amp;gt;= MOD)
			A[Y[i]] -= MOD;
	}
	for (int i=1;i&amp;lt;=L;i++){
		int n; cin &amp;gt;&amp;gt; n;
		int ans = 0;
		for (int j=0;j&amp;lt;n;j++){
			int a, b; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;
			ans = (ans+(lld)A[a]*b)%MOD;
		}
		cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1519 D. 적절한 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$x$ 좌표가 $1$ 이상 $W$ 이하이며, $y$ 좌표가 $1$ 이상 $H$ 이하인 2차원 정수 좌표 공간이 있다. 여기에 $K$ 개의 점 집합이 있다. 그리고 각 집합에 대해 $C_i$ 값이 입력으로 주어진다. 적절한 점이란, 모든 $i$에 대해 점 집합 $i$에서 그 점보다 $x$ 좌표가 같거나 작고, 그 점보다 $y$ 좌표가 같거나 작은 점의 개수가 정확히 $C_i$가 되는 점을 말한다. 이때, 적절한 점의 개수를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀이의 아이디어는 간단하다. 어떤 $x$ 좌표에 대해 적절한 점은 연속된 구간으로 표현된다. 각 점 집합에 대해 독립적으로 적절한 점의 $y$ 좌표 구간을 구하고, 교집합을 구하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌표 압축과 자료 구조를 이용하여 빠른 시간복잡도로 구현할 수 있다. 전체 점의 개수를 $N$이라고 했을 때, 풀이의 시간복잡도는 $O(N \lg N)$이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

int W, H, K;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	cin &amp;gt;&amp;gt; W &amp;gt;&amp;gt; H &amp;gt;&amp;gt; K;
	vector&amp;lt;tuple&amp;lt;int, int, int&amp;gt;&amp;gt; A;
	A.emplace_back(1, 0, 0);
	A.emplace_back(W+1, 0, 0);
	for (int i=1;i&amp;lt;=K;i++){
		int n; cin &amp;gt;&amp;gt; n;
		for (int j=0;j&amp;lt;n;j++){
			int x, y; cin &amp;gt;&amp;gt; x &amp;gt;&amp;gt; y;
			A.emplace_back(x, y, i);
		}
	}
	sort(begin(A), end(A));

	struct PointSet{
		map&amp;lt;int, int&amp;gt; val = {{1, 0}};
		int bound, cnt = 0, pivot = 0, nxt;
		void add(int y){
			val[y]++;
			if (!pivot) pivot = y, nxt = H+1;
			if (y &amp;lt;= pivot) cnt++;
			auto it = val.find(pivot);
			while (cnt &amp;gt; bound &amp;amp;&amp;amp; it != val.begin()){
				cnt -= it-&amp;gt;second;
				it--;
			}
			while (cnt &amp;lt; bound){
				auto nxt = it; nxt++;
				if (nxt == val.end() || cnt+nxt-&amp;gt;second &amp;gt; bound)
					break;
				cnt += nxt-&amp;gt;second;
				it = nxt;
			}
			pivot = it-&amp;gt;first;
			it++;
			nxt = it == val.end() ? H+1 : it-&amp;gt;first;
		}
	};
	PointSet ps[K+1];
	for (int i=1;i&amp;lt;=K;i++){
		int v; cin &amp;gt;&amp;gt; v;
		ps[i].bound = v;
	}

	priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; P, Q;
	int S[K+1], E[K+1];
	for (int i=1;i&amp;lt;=K;i++){
		if (!ps[i].bound) S[i] = 1, E[i] = H+1;
		else S[i] = 1, E[i] = 0;
		P.emplace(S[i], i);
		Q.emplace(-E[i], i);
	}
	auto update = [&amp;amp;](int i, int s, int e){
		if (S[i] == s &amp;amp;&amp;amp; E[i] == e) return;
		S[i] = s; E[i] = e;
		P.emplace(S[i], i);
		Q.emplace(-E[i], i);
	};
	lld ans = 0;
	int prv = 0;
	for (auto [x, y, i]: A){
		if (prv &amp;amp;&amp;amp; prv != x){
			while (S[P.top().second] != P.top().first) P.pop();
			while (E[Q.top().second] != -Q.top().first) Q.pop();
			int s = P.top().first;
			int e = -Q.top().first;
			if (s &amp;lt; e) ans += (lld)(e-s)*(x-prv);
		}
		prv = x;
		if (!i) continue;
		ps[i].add(y);
		if (ps[i].cnt == ps[i].bound) update(i, ps[i].pivot, ps[i].nxt);
		else update(i, 1, 0);
	}
	cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1519 E. 지름길&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정점 $N$ 개와 양방향 간선 $M$ 개로 이루어진 그래프가 주어진다. 각 간선에는 길이가 있다. 정점 $S$와 정점 $T$가 주어진다. 정점 $S$에서 정점 $i$로 가는 최단 거리와 정점 $i$에서 정점 $T$로 가는 최단 거리가 유지되도록 최소 개수의 간선만 남기는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제 풀이에 대한 접근은 최단 거리 트리(Shortest-path tree)를 생각해보면 된다. 정점 $S$에 대한 최단 거리 트리와 정점 $T$에 대한 최단 거리 트리가 원래 그래프와 동일하도록 최소 개수의 간선만 남기면 된다. 이 점을 생각해보면, 문제에서 요구하는 답은 최대 $2(N-1)$이라는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소 개수의 간선을 남겨야하므로, 최대한 많은 간선이 정점 $S$에 대한 최단 거리 트리와 정점 $T$에 대한 최단 거리 트리에 동시에 존재하는 것이 유리하다. 두 최단 거리 트리에 모두 존재할 수 있는 간선 후보들을 구한 뒤, 그 후보 중에서 동시에 존재할 수 있는 간선의 최대 개수를 구하면 된다. 동시에 존재할 수 있는 간선의 최대 개수는 이분 그래프에서 최대 매칭을 통해 구할 수 있다. 최대 매칭의 개수가 $F$라고 하면, 답은 $2(N-1) - F$가 된다. 이분 매칭은 Hopcroft-Karp 알고리즘을 구하는 것이 가장 빠르며 이 문제를 해결하는 전체 시간복잡도는 $O(N + M \sqrt{N})$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀이의 원리를 이해했다면, 남겨야하는 간선을 구하는 것은 간단하다. 첨부된 코드를 참고하자.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

constexpr lld inf = 1e18;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M, S, T; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M &amp;gt;&amp;gt; S &amp;gt;&amp;gt; T;
	vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; con[N+1];
	tuple&amp;lt;int, int, int&amp;gt; edges[M+1];
	for (int i=1;i&amp;lt;=M;i++){
		int a, b, c; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
		con[a].emplace_back(b, c);
		con[b].emplace_back(a, c);
		edges[i] = {a, b, c};
	}
	lld D[2][N+1];
	auto dijkstra = [&amp;amp;](int st, lld dist[]){
		fill(dist+1, dist+N+1, inf);
		priority_queue&amp;lt;pair&amp;lt;lld, int&amp;gt;&amp;gt; que;
		dist[st] = 0; que.emplace(0, st);
		while (!que.empty()){
			auto [d, n] = que.top(); que.pop();
			if (dist[n] != -d) continue;
			for (auto [t, v]: con[n]){
				if (dist[t] &amp;gt; dist[n]+v){
					dist[t] = dist[n]+v;
					que.emplace(-dist[t], t);
				}
			}
		}
	};
	dijkstra(S, D[0]);
	dijkstra(T, D[1]);

	int selectedA[N+1]{}, selectedB[N+1]{};
	vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; bcon[N+1];
	for (int i=1;i&amp;lt;=M;i++){
		auto [a, b, c] = edges[i];
		int p = 0, q = 0;
		if (D[0][a]-D[0][b] == c) p = a;
		if (D[0][b]-D[0][a] == c) p = b;
		if (D[1][a]-D[1][b] == c) q = a;
		if (D[1][b]-D[1][a] == c) q = b;
		if (p &amp;amp;&amp;amp; q) bcon[p].emplace_back(q, i);
		if (p) selectedA[p] = i;
		if (q) selectedB[q] = i;
	}

	bool used[N+1]{}; int match[N+1]{}, matchE[N+1]{}, dist[N+1];
	auto bfs = [&amp;amp;](){
		fill(dist+1, dist+N+1, 1e9);
		queue&amp;lt;int&amp;gt; que;
		for (int i=1;i&amp;lt;=N;i++) if (!used[i]) que.push(i);
		while (!que.empty()){
			int q = que.front(); que.pop();
			for (auto [t, _]: bcon[q]){
				if (int m = match[t]; m &amp;amp;&amp;amp; dist[m] == 1e9)
					dist[m] = dist[q]+1, que.push(m);
			}
		}
	};
	function&amp;lt;bool(int)&amp;gt; dfs = [&amp;amp;](int n){
		for (auto [t, e]: bcon[n]){
			if (int m = match[t]; !m || dist[m] == dist[n]+1 &amp;amp;&amp;amp; dfs(m)){
				used[n] = 1;
				match[t] = n;
				matchE[t] = e;
				return 1;
			}
		}
		dist[n] = 1e9;
		return 0;
	};
	int matched = 0;
	for (;;){
		bfs();
		int flow = 0;
		for (int i=1;i&amp;lt;=N;i++) if (!used[i] &amp;amp;&amp;amp; dfs(i)) flow++;
		if (!flow) break;
		matched += flow;
	}

	int K = 2*(N-1)-matched;
	cout &amp;lt;&amp;lt; K &amp;lt;&amp;lt; '\n';

	for (int i=1;i&amp;lt;=N;i++) if (match[i])
		selectedA[match[i]] = selectedB[i] = matchE[i];
	bool edge_used[M+1]{};
	for (int i=1;i&amp;lt;=N;i++) edge_used[selectedA[i]] = edge_used[selectedB[i]] = 1;
	for (int i=1;i&amp;lt;=M;i++) if (edge_used[i]) cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; ' ';
	cout &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>해법</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/146</guid>
      <comments>https://mwchun.tistory.com/146#entry146comment</comments>
      <pubDate>Mon, 31 Oct 2022 00:05:09 +0900</pubDate>
    </item>
    <item>
      <title>Nexon Youth Programming Challenge(NYPC) 2022 Round 2-B 풀이</title>
      <link>https://mwchun.tistory.com/145</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 비트문자열&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 규칙으로 만들어지는 길이 $2^i$인 이진문자열 $S_i$가 있다. 구간 $[s, e]$가 있을 때, $S_i$의 $s$ 번째 문자부터 $e$ 번째 문자까지 문자 중에서 1의 개수를 구하는 문제다. 단, 하나의 입력 파일에 최대 20만 개의 테스트 케이스가 들어올 수 있어서 상당히 빠른 시간 안에 문제를 해결해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 설명에 적힌 규칙과 다른 방식으로 $S_i$를 설명할 수 있다. $S_0$는 &quot;0&quot;이며, $1$ 이상인 $i$에 대해 $S_i$는 $S_{i-1}$에서 0/1을 뒤집은 것과 $S_{i-1}$을 이어 붙인 문자열이 된다. 즉, $S_1$은 &quot;10&quot;이며, $S_2$는 &quot;10&quot;에서 0/1을 뒤집은 &quot;01&quot;에 &quot;10&quot;을 이어 붙인 &quot;0110&quot;이 된다. $S_3$은 &quot;0110&quot;에서 0/1을 뒤집은 &quot;1001&quot;에 &quot;0110&quot;을 이어붙인 &quot;10010110&quot;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$S_i$는 여러 특징이 있는데, 이 문제를 풀면서 필요한 특징은 두 가지이다. 하나는 $k &amp;gt; 0$인 정수 $k$에 대해 문자열 $S_i$의 $2k$ 번째 문자와 $2k-1$ 번째 문자는 항상 다르다는 특징이다. 이 특징을 이용하면 $s$ 번째 문자와 $e$ 번째 문자만 알면, 구간 $[s, e]$에 있는 1의 개수를 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 어떤 $x$에 대해, $S_i$에서 $x$ 번째 문자를 빠른 시간 안에 구하면 이 문제를 해결할 수 있다. 어떤 $x$에 대해 $S_i$의 $x$ 번째 문자와 $S_{i+1}$의 $x$ 번째 문자는 항상 다르다는 점을 이용하면 된다. 그러면 $i &amp;gt; 60$인 경우, $S_{60}$의 $x$ 번째 문자를 구하고, $i$의 홀짝성을 이용해서 $S_i$의 $x$ 번째 문자도 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로 설명해보니 방법이 좀 복잡해 보이는데, 실제로는 간단하다. 아래 코드를 참고하자.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

bool get(lld K, lld x){
	if (K == 0) return 0;
	if (K &amp;gt; 60){
		bool ret = get(60, x);
		if (K%2 == 1) ret ^= 1;
		return ret;
	}
	lld n = 1LL &amp;lt;&amp;lt; K, m = n/2;
	if (x &amp;gt; m) return get(K-1, x-m);
	return get(K-1, x)^1;
}

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int T; cin &amp;gt;&amp;gt; T;
	while (T--){
		lld K, s, e; cin &amp;gt;&amp;gt; K &amp;gt;&amp;gt; s &amp;gt;&amp;gt; e;
		lld ans = 0;
		if (e%2 == 1) ans += get(K, e--);
		if (s%2 == 0) ans += get(K, s++);
		ans += (e-s+1)/2;
		cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 정수 놀이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 $Q$ 개 주어진다. 각 쿼리에 대해 양의 정수 $A$에 최소 몇 번의 연산을 적용하여 양의 정수 $B$를 만들 수 있는지를 구해야 한다. 적용할 수 있는 연산은 1) 정수에 1을 더한다, 2) 정수에 1을 뺀다, 3) 정수를 제곱한다, 4) 정수가 제곱 수라면 제곱근을 한다. 단, 연산 이후에도 양의 정수가 되는 경우에만 연산을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 문제에서 주어진 네 종류의 연산은 서로 대칭이다. 따라서 문제를 양의 정수 $A$와 $B$가 주어졌을 때, 두 정수에 최소 횟수의 연산을 적용하여 같은 값으로 만드는 문제로 바꿀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$A$와 $B$ 중 큰 값의 수준을 하나씩 줄여가면서 답을 갱신해줄 것이다. 여기서 어떤 수의 수준을 줄인다는 것은 그 수를 가까운 제곱수로 만든 뒤, 제곱근 연산을 한다는 말이다. 일반성을 잃지 않고 $A \ge B$라고 하자. 현재 단계에서 가능한 답의 후보는 $|A-B|$이다. $A$를 가까운 제곱수로 만든 뒤, 제곱근을 구하면 $C$가 된다고 하자. 이때, 필요한 연산 횟수는 $|C^2-A|+1$이다. 그 이후 단계에서 가능한 답의 후보는 $|C^2-A|+1+|C-B|$가 된다. 이런 식으로 한 단계마다 큰 수의 수준을 하나씩 줄여가면서 답의 후보를 구하면, 그 후보들 중에 문제의 정답이 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

int isqrt(lld v){
	int s = 1, e = 1e9, t = 0;
	while (s &amp;lt;= e){
		int m = s+e &amp;gt;&amp;gt; 1;
		if ((lld)m*m &amp;lt;= v) s = m+1, t = m;
		else e = m-1;
	}
	return t;
}

lld solve(lld a, lld b){
	if (a &amp;lt; b) return solve(b, a);
	if (a == b) return 0;
	lld ret = a-b;
	lld k = isqrt(a);
	if (a &amp;lt;= k*(k+1))
		ret = min(ret, solve(k, b) + a - k*k + 1);
	else
		ret = min(ret, solve(k+1, b) + (k+1)*(k+1) - a + 1);
	return ret;
}

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int T; cin &amp;gt;&amp;gt; T;
	while (T--){
		lld a, b; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;
		cout &amp;lt;&amp;lt; solve(a, b) &amp;lt;&amp;lt; '\n';
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 물풍선 애널리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한한 2차원 격자판의 일부 칸에 물풍선이 놓여있다. 물풍선 $i$의 위치는 $(x_i, y_i)$이며 세기는 $p_i$이다. 물풍선은 십자 모양으로 터진다. $t_i$는 물풍선 $i$를 터트렸을 때, 연쇄 작용을 통해 모든 물풍선이 터지는지 여부를 나타낸다. $p_i$에 대한 정보를 잃어버린 상황에서 $t_i$의 정보를 가지고 $p_i$ 값들을 복원하는 문제다. 만약 가능한 답이 여러 가지인 경우, 그중 아무거나 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 $t_i = 1$이라면, 즉, 물풍선 $i$를 터트렸을 때, 연쇄 작용을 통해 모든 물풍선이 터진다면, $p_i$ 값은 크면 클수록 좋다. 만약 $t_i = 0$이라면, 물풍선 $i$를 터트렸을 때 $t_j = 1$인 물풍선 $j$를 터트리면 안 된다. 그런 물풍선을 터트리지 않는 조건에서 $p_i$가 크면 클수록 좋다. 이렇게 $p_i$들 중 가장 큰 값들을 구하면 답이 된다. $t_i$ 값을 만족하는 $p_i$ 조합이 항상 존재하도록 입력이 주어지기 때문에, 이렇게 구한 $p_i$가 $t_i$ 조건을 만족하는지 따로 체크하지 않아도 된다. 한 가지 예외 처리가 필요하다. 만약 모든 $t_i$가 $0$이라면, $p_i$는 모두 1이 되어야 한다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

constexpr int MAX = 1e9;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N; cin &amp;gt;&amp;gt; N;
	struct Bomb{
		int x, y, t, idx;
	};
	Bomb A[N+1];
	int P[N+1];
	bool all_zero = 1;
	for (int i=1;i&amp;lt;=N;i++){
		cin &amp;gt;&amp;gt; A[i].x &amp;gt;&amp;gt; A[i].y &amp;gt;&amp;gt; A[i].t;
		A[i].idx = i; P[i] = MAX;
		if (A[i].t) all_zero = 0;
	}

	if (all_zero){
		for (int i=1;i&amp;lt;=N;i++) cout &amp;lt;&amp;lt; &quot;1\n&quot;;
		return 0;
	}

	auto put_min = [](auto&amp;amp; a, auto b){
		a = min(a, b);
	};

	sort(A+1, A+N+1, [](const Bomb&amp;amp; a, const Bomb&amp;amp; b){
		return a.y != b.y ? a.y &amp;lt; b.y : a.x &amp;lt; b.x;
	});
	int last = 0;
	for (int i=1;i&amp;lt;=N;i++){
		if (A[i].t) last = i;
		else if (last &amp;amp;&amp;amp; A[last].y == A[i].y) put_min(P[A[i].idx], A[i].x-A[last].x-1);
	}
	last = 0;
	for (int i=N;i;i--){
		if (A[i].t) last = i;
		else if (last &amp;amp;&amp;amp; A[last].y == A[i].y) put_min(P[A[i].idx], A[last].x-A[i].x-1);
	}

	sort(A+1, A+N+1, [](const Bomb&amp;amp; a, const Bomb&amp;amp; b){
		return a.x != b.x ? a.x &amp;lt; b.x : a.y &amp;lt; b.y;
	});
	last = 0;
	for (int i=1;i&amp;lt;=N;i++){
		if (A[i].t) last = i;
		else if (last &amp;amp;&amp;amp; A[last].x == A[i].x) put_min(P[A[i].idx], A[i].y-A[last].y-1);
	}
	last = 0;
	for (int i=N;i;i--){
		if (A[i].t) last = i;
		else if (last &amp;amp;&amp;amp; A[last].x == A[i].x) put_min(P[A[i].idx], A[last].y-A[i].y-1);
	}

	for (int i=1;i&amp;lt;=N;i++) cout &amp;lt;&amp;lt; P[i] &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 멘토링 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; $N$ 명의 멘토가 있고, $N$ 명의 멘티가 있다. 멘토링이 가능한 멘토와 멘티 쌍들이 주어졌을 때, 1:1 매칭을 해줄 수 있으며, 그 방법이 유일한지 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 이분그래프에서 완벽 매칭의 유일성을 묻는 문제다. 만약, Hopcroft-Karp 등의 방법을 통해서 최대 매칭을 구했다고 하자. 만약 최대 매칭이 완전 매칭이 아니라면, 즉, 매칭 수가 $N$이 아니라면 답은 &quot;NO&quot;다. 임의의 완전 매칭을 구했을 때, 이 완전 매칭이 유일한지 검사가 추가적으로 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 가장 먼저 떠오를 수 있는 방법으로는 간선의 순서를 랜덤하게 섞어서 여러 번 매칭을 구해서 구한 매칭이 모두 같은지 확인하는 방법이다. 매칭 알고리즘을 여러 번 돌리기 때문에 시간 초과가 날 수 있으며, 확률적으로 올바르지 않은 답을 구하게 된다. 이 방법으로는 꽤 낮은 확률로 이 문제를 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방법으로, 이분그래프를 방향성 그래프로 만든 뒤, 임의로 구한 완전 매칭에 속한 간선의 방향을 뒤집는다. 이렇게 만들어진 방향성 그래프에서 사이클이 있는지 확인한다. 만약, 사이클이 있다면 다른 매칭도 존재할 수 있다. 싸이클이 존재하지 않는다면, 완전 매칭은 유일하다. 사이클의 존재 여부 확인은 위상 정렬을 통해 $O(V+E)$ 시간복잡도로 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법은 이분 매칭으로 완전 매칭을 구해야 하므로, 이분 매칭 구현이 아주 빠르지 않은 이상 시간초과가 나올 수 있다. Hopcroft-Karp 알고리즘이 꽤나 빠른 이분매칭 알고리즘인데, $O(E \sqrt{V})$ 시간복잡도를 가진다. Hopcroft-Karp 알고리즘이 최악의 시간을 가지도록 하는 입력 데이터가 잘 알려져 있지 않아, 이 문제에서는 Hopcroft-Karp 알고리즘 1번 돌리는 것으로 시간초과가 나오지 않는다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M;
	vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; con(N+N+1);
	for (int i=0;i&amp;lt;M;i++){
		int a, b; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;
		con[a].push_back(b);
	}

	vector&amp;lt;int&amp;gt; used(N+1), matched(N+1), dist(N+1);
	auto bfs = [&amp;amp;](){
		queue&amp;lt;int&amp;gt; que;
		fill(begin(dist), end(dist), 1e9);
		for (int i=1;i&amp;lt;=N;i++) if (!used[i]) dist[i] = 0, que.push(i);
		while (!que.empty()){
			int q = que.front(); que.pop();
			for (int t: con[q]){
				int m = matched[t];
				if (m &amp;amp;&amp;amp; dist[m] == 1e9) dist[m] = dist[q]+1, que.push(m);
			}
		}
	};

	function&amp;lt;bool(int)&amp;gt; dfs = [&amp;amp;](int n) -&amp;gt; bool{
		for (int t: con[n]){
			int m = matched[t];
			if (!m || dist[m] == dist[n]+1 &amp;amp;&amp;amp; dfs(m)){
				used[n] = t; matched[t] = n;
				return 1;
			}
		}
		dist[n] = 1e9;
		return 0;
	};

	int cnt = 0;
	for (;;){
		bfs();
		int flow = 0;
		for (int i=1;i&amp;lt;=N;i++) if (!used[i] &amp;amp;&amp;amp; dfs(i)) flow++;
		if (!flow) break;
		cnt += flow;
	}

	if (cnt &amp;lt; N) return puts(&quot;NO&quot;), 0;

	vector&amp;lt;int&amp;gt; indeg(N+N+1);
	vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; ncon(N+N+1);
	for (int i=1;i&amp;lt;=N;i++){
		for (int j: con[i]){
			if (used[i] == j) ncon[N+j].push_back(i), indeg[i]++;
			else ncon[i].push_back(N+j), indeg[N+j]++;
		}
	}
	int visited = 0;
	queue&amp;lt;int&amp;gt; que;
	for (int i=1;i&amp;lt;=N+N;i++) if (!indeg[i]) que.push(i);
	while (!que.empty()){
		int q = que.front(); que.pop();
		visited++;
		for (int t: ncon[q]){
			if (!--indeg[t]) que.push(t);
		}
	}
	if (visited &amp;lt; N+N) return puts(&quot;NO&quot;), 0;

	cout &amp;lt;&amp;lt; &quot;YES\n&quot;;
	for (int i=1;i&amp;lt;=N;i++) cout &amp;lt;&amp;lt; used[i] &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 문제는 완전 매칭이 가능한지 확인하고, 가능하다면 그 방법이 유일한지만 구하는 문제이므로 일반적인 이분매칭 알고리즘 없이 $O(V+E)$ 시간에 해결할 수 있다. 증명을 생략하고 방법만 설명하겠다. 주어진 이분그래프에서 차수가 1인 정점을 살펴보자. 차수가 1인 정점에 대해서 유일한 간선으로 연결된 정점과 매칭 해주는 과정을 반복한다. 만약, 원래 그래프에서 유일한 완전 매칭이 존재한다면 이 방법으로 유일한 완전 매칭을 찾을 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M;
	vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; con(N+N+1);
	for (int i=0;i&amp;lt;M;i++){
		int a, b; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;
		con[a].push_back(N+b);
		con[N+b].push_back(a);
	}

	int cnt = 0;
	int matched[N+N+1]{}, deg[N+N+1]{};
	queue&amp;lt;int&amp;gt; que;
	for (int i=1;i&amp;lt;=N+N;i++){
		deg[i] = size(con[i]);
		if (deg[i] == 1) que.push(i);
	}
	while (!que.empty()){
		int q = que.front(); que.pop();
		if (matched[q]){
			for (int t: con[q]) if (!matched[t]){
				if (--deg[t] == 1) que.push(t);
			}
		}else{
			for (int t: con[q]) if (!matched[t]){
				matched[q] = t;
				matched[t] = q;
				que.push(t);
				cnt++;
			}
		}
	}
	if (cnt &amp;lt; N) return puts(&quot;NO&quot;), 0;
	cout &amp;lt;&amp;lt; &quot;YES\n&quot;;
	for (int i=1;i&amp;lt;=N;i++) cout &amp;lt;&amp;lt; matched[i]-N &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>해법</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/145</guid>
      <comments>https://mwchun.tistory.com/145#entry145comment</comments>
      <pubDate>Sat, 3 Sep 2022 18:01:18 +0900</pubDate>
    </item>
    <item>
      <title>Nexon Youth Programming Challenge(NYPC) 2022 Round 2-A 풀이</title>
      <link>https://mwchun.tistory.com/144</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 사진작가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 정수로 이루어진 길이 $N$인 배열 $A$가 주어진다. 이 배열의 부분배열 중에서 같은 수를 여러 개 포함하고 있지 않은 부분배열이 있다. 그러한 부분배열 중에서 길이가 가장 큰 부분배열의 길이를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 구성하는 정수의 범위가 $1$ 이상 $1\,000\,000$ 이하이다. 크기가 $1\,000\,000$인 배열을 하나 잡아서 마지막으로 그 수가 등장한 인덱스를 저장하면, 어떤 수 $i$에 대해, $A_i$랑 같은 수 중 왼쪽에 있으면서 가장 오른쪽에 있는 수의 인덱스 $last_i$를 구할 수 있다. 그다음, 오른쪽 끝이 $i$인 부분배열 중에서 문제의 조건을 만족하는 가장 큰 부분배열의 왼쪽 끝은 $\max\limits_{1 \le j \le i}(last_i)+1$이 된다. 이를 이용하여 시간복잡도 $O(N+\max(A_i))$에 해결할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N; cin &amp;gt;&amp;gt; N;
	int A[N], X = 0;
	for (int&amp;amp; v: A) cin &amp;gt;&amp;gt; v, X = max(X, v);

	int ans = 0;
	vector&amp;lt;int&amp;gt; last(X+1, -1);
	for (int i=0,j=0;i&amp;lt;N;i++){
	    j = max(j, last[A[i]]+1);
		ans = max(ans, i-j+1);
		last[A[i]] = i;
	}
	cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 리본&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N$ 개의 리본이 일직선 막대기 위에 매달려 있다. 왼쪽에서 $i$ 번째 리본이 매달린 위치는 $X_i$, 리본의 길이는 $A_i$, 리본의 가중치는 $B_i$이다. 두 리본을 적절히 선택하여 묶어줄 수 있는데, 만약 리본 $i$와 리본 $j$를 묶어주면 점수를 $B_i \times B_j$ 만큼 얻을 수 있다. 한 리본은 최대 한 개의 리본과만 묶을 수 있고, 어떤 리본과도 묶이지 않은 리본의 점수는 $B_i$다. 묶을 리본을 모두 묶은 후 모양에서 서로 묶인 리본을 제외한 어떠한 리본도 서로 닿아서는 안 된다. 적절히 리본을 묶어 최대 점수를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 2차원 DP를 생각해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D[i][j] = 리본 $i$부터 리본 $j$까지 총 $j-i+1$ 개의 리본이 있고, 리본 $i$와 리본 $j$를 묶었을 때 최대 점수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 DP 배열을 정의하면 상수가 작은 시간복잡도 $O(N^4)$에 해결할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N; cin &amp;gt;&amp;gt; N;
	int X[N+2]{}, A[N+2]{}, B[N+2]{};
	for (int i=1;i&amp;lt;=N;i++) cin &amp;gt;&amp;gt; X[i];
	for (int i=1;i&amp;lt;=N;i++) cin &amp;gt;&amp;gt; A[i];
	for (int i=1;i&amp;lt;=N;i++) cin &amp;gt;&amp;gt; B[i];
	lld D[N+2][N+2]; int H[N+2][N+2];
	for (int i=0;i&amp;lt;=N+1;i++) for (int j=0;j&amp;lt;=N+1;j++)
		D[i][j] = H[i][j] = -1;
	for (int i=1;i&amp;lt;=N;i++) for (int j=i;j&amp;lt;=N;j++)
		H[i][j] = A[j]+A[i]-(X[j]-X[i]);
	for (int i=1;i&amp;lt;=N;i++) D[i][i] = B[i];
	for (int i=1;i&amp;lt;N;i++) if (X[i+1]-X[i] &amp;lt;= A[i+1]+A[i])
		D[i][i+1] = (lld)B[i]*B[i+1];
	H[0][N+1] = 2e9+1;
	for (int z=2;z&amp;lt;=N+1;z++){
		for (int i=0;i+z&amp;lt;=N+1;i++){
			int j = i+z;
			if (H[i][j] &amp;lt; 0) continue;
			vector&amp;lt;lld&amp;gt; dy(N+1, -1);
			for (int k=i+1;k&amp;lt;j;k++){
				if (H[i+1][k] &amp;lt; H[i][j])
					dy[k] = D[i+1][k];
			}
			for (int k=i+1;k&amp;lt;j;k++) if (dy[k] &amp;gt;= 0){
				for (int l=k+1;l&amp;lt;j;l++) if (D[k+1][l] &amp;gt;= 0 &amp;amp;&amp;amp; H[k+1][l] &amp;lt;= H[i][j])
					dy[l] = max(dy[l], dy[k]+D[k+1][l]);
			}
			if (dy[j-1] &amp;lt; 0) continue;
			D[i][j] = dy[j-1] + (lld)B[i]*B[j];
		}
	}
	cout &amp;lt;&amp;lt; D[0][N+1] &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 로봇청소기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N \times M$ 크기의 2차원 격자판에 O 혹은 X가 적혀있다. X가 적힌 칸은 청소가 필요한 칸을 의미한다. 로봇청소기는 첫 행의 임의의 칸에서 시작하여 왼쪽 대각선(L), 오른쪽 대각선(R), 혹은 아래(D) 칸으로 이동할 수 있다. 로봇청소기가 마지막 행에 도착하면 이동을 멈춘다. 이러한 일련의 작업을 청소 한 번이라고 정의한다. 청소가 필요한 모든 칸에 방문하기 위해 필요한 최소 청소 횟수를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;칸 $i$의 좌표를 $(r_i, c_i)$라고 하자. $p_i = r_i-c_i$, $q_i = r_i+c_i$라고 하자. 로봇청소기가 칸 $i$에 방문하고 나서 칸 $j$에 방문할 수 있는 필요충분조건은 $p_i \le p_j \wedge q_i \le q_j$이다. 이제 청소가 필요한 모든 칸을 $(p_i, q_i)$로 정렬하고 나서, $q$ 배열을 생각하자. $q$ 배열에서 단조 증가 부분배열 하나를 생각하면 로봇청소기가 한 번 청소할 때 방문하는 칸들을 의미하게 된다. 즉, $q$ 배열에서 단조 증가 부분배열 하나를 찾아서 그 원소를 모두 지우는 작업 한 번이, 로봇청소기의 청소 한 번이 된다. 그렇다면 문제는 Dilworth's theorem에 의해 $q$ 배열에서 최장 감소 부분배열의 길이가 된다. 이 원리를 이해하면, 로봇청소기의 이동 경로 역추적은 어렵지 않게 할 수 있다. 시간복잡도는 $O(NM \lg M)$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 구현할 때, 청소가 필요한 모든 칸을 $(p_i, q_i)$ 기준으로 정렬할 필요가 없다. 자세한 구현은 아래 첨부된 코드를 참고하자.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M;
	char A[N+1][M+2];
	for (int i=1;i&amp;lt;=N;i++) cin &amp;gt;&amp;gt; (A[i]+1);
	// p_i = r_i-c_i
	// q_i = r_i+c_i
	// i -&amp;gt; j &amp;lt;=&amp;gt; (p_i &amp;lt;= p_j &amp;amp;&amp;amp; q_i &amp;lt;= q_j)
	vector&amp;lt;int&amp;gt; arr, init;
	vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; pos;
	vector&amp;lt;string&amp;gt; path;
	for (int p=1-M;p&amp;lt;=N-1;p++){
		int r = 1, c = r-p;
		if (c &amp;lt; 1) r += 1-c, c = 1;
		for (;r&amp;lt;=N&amp;amp;&amp;amp;c&amp;lt;=M;r++,c++){
			if (A[r][c] != 'X') continue;
			int v = -(r+c);
			auto it = lower_bound(begin(arr), end(arr), v);
			if (it == end(arr)){
				arr.push_back(v);
				pos.emplace_back(r, c);
				init.push_back(c);
				path.emplace_back(r-1, 'D');
			}
			else{
				int i = it-begin(arr);
				arr[i] = v;
				int dr = r-pos[i].first, dc = c-pos[i].second;
				pos[i] = {r, c};
				if (dc &amp;lt; 0) path[i] += string(-dc, 'L');
				else path[i] += string(dc, 'R');
				path[i] += string(dr-abs(dc), 'D');
			}
		}
	}
	int K = size(arr);
	cout &amp;lt;&amp;lt; K &amp;lt;&amp;lt; '\n';
	for (int i=0;i&amp;lt;K;i++){
		path[i] += string(N-pos[i].first, 'D');
		cout &amp;lt;&amp;lt; init[i] &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; path[i] &amp;lt;&amp;lt; '\n';
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 물고기 양식장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N \times M$ 크기의 2차원 격자판이 있다. $Q$ 개의 쿼리가 주어지는데, $i$ 번째 쿼리는 시간 $i$에 물고기 $k_i$가 어떤 부분 직사각형 영역 안에 입장하거나, 어떤 부분 직사각형 영역에서 퇴장하는 것을 의미한다. 물고기는 항상 입장한 부분 직사각형 영역과 동일한 부분 직사각형 영역에서 퇴장하고, 입장하지도 않은 물고기가 퇴장하거나, 입장한 물고기가 퇴장하지 않는 입력은 주어지지 않는다. 어떤 시간 $i$에서 $i+1$로 넘어가는 시점에, 서로 다른 두 물고기가 같은 칸을 공유하면, 그 칸에서 각 물고기는 알을 한 개씩 낳는다. 즉, 어떤 시점에 서로 다른 두 물고기가 $5$ 개의 칸을 공유한다면 그 시점에 각 물고기는 $5$ 개의 알을 낳는다. 출입 쿼리가 모두 주어졌을 때, 각 물고기가 낳은 알의 개수를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 2차원 문제처럼 보이지만, 시간이라는 개념 때문에 사실 3차원 문제가 된다. 물고기의 출입 한 건을 3차원 공간에서 하나의 직육면체로 나타낼 수 있고, 직육면체의 교집합의 부피가 낳은 알의 개수가 된다. 이는 2차원 Fenwick tree $2^3$ 개를 사용하면 해결할 수 있다. 종류 3은 $N \le 10$으로 이 문제를 2차원 $10$ 개를 해결하는 문제가 된다. 처음부터 3차원에서 생각하는 것이 어려운 사람들은 2차원에서 이 문제를 해결하고 비슷한 풀이를 확장하여 3차원에서 사용할 수 있다. 여담으로 Fenwick tree의 개수가 $2^3$이 되는 이유는, 3차원에서 변수가 시간, 행, 열 $3$ 개 있고, 각 변수에 대해 들어가는 항, 들어가지 않는 항을 모두 생각하면 총 $2^3$ 개의 항이 있음을 알 수 있다. 그 각 항에 대해서 2차원 연산을 하여 결과를 종합하면 이 문제를 해결할 수 있다. 시간복잡도는 상수가 좀 많이 큰 $O(K + NM + Q \lg N \lg M)$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 엄밀하게 말해서 문제의 풀이가 아니라 힌트에 가깝다. 충분한 힌트가 될 수 있으므로 이후 과정은 한번 생각해보길 바란다. 그 이후 더 자세한 힌트나 구현은 아래 코드를 참고하자.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:python;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

template&amp;lt;typename T&amp;gt;
struct Fenwick2D{
	Fenwick2D(int n, int m): n(n), m(m), tree(n+1, vector&amp;lt;T&amp;gt;(m+1)){}
	void clear(){
		for (int i=1;i&amp;lt;=n;i++) fill(begin(tree[i]), end(tree[i]), T());
	}
	void add(int y, int x, T v){
		for (;y&amp;lt;=n;y+=y&amp;amp;-y){
			for (int i=x;i&amp;lt;=m;i+=i&amp;amp;-i) tree[y][i] += v;
		}
	}
	T get(int y, int x){
		T ret = 0;
		for (;y;y^=y&amp;amp;-y){
			for (int i=x;i;i^=i&amp;amp;-i) ret += tree[y][i];
		}
		return ret;
	}
	int n, m;
	vector&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; tree;
};

int main(){
	cin.tie(0)-&amp;gt;sync_with_stdio(0);
	int N, M, K, Q; cin &amp;gt;&amp;gt; N &amp;gt;&amp;gt; M &amp;gt;&amp;gt; K &amp;gt;&amp;gt; Q;
	tuple&amp;lt;int, int, int, int&amp;gt; QR[Q+1][4];
	lld ans[K+1]{}; int in_ts[K+1]{};
	for (int i=1;i&amp;lt;=Q;i++){
		int t, k, r1, c1, r2, c2;
		cin &amp;gt;&amp;gt; t &amp;gt;&amp;gt; k &amp;gt;&amp;gt; r1 &amp;gt;&amp;gt; c1 &amp;gt;&amp;gt; r2 &amp;gt;&amp;gt; c2;
		int s = (t == 1 ? 1 : -1);
		QR[i][0] = {r1, c1, k, s};
		QR[i][1] = {r1, c2+1, k, -s};
		QR[i][2] = {r2+1, c1, k, -s};
		QR[i][3] = {r2+1, c2+1, k, s};
		if (t == 1) in_ts[k] = i;
		else ans[k] -= (lld)(i-in_ts[k])*(r2-r1+1)*(c2-c1+1);
	}
	Fenwick2D&amp;lt;lld&amp;gt; fw(N, M);
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] += -s * fw.get(r-1, c-1) * i * r * c;
		fw.add(r, c, (lld)s);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] -= -s * fw.get(r-1, c-1) * r * c;
		fw.add(r, c, (lld)s*i);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] -= -s * fw.get(r-1, c-1) * i * c;
		fw.add(r, c, (lld)s*r);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] -= -s * fw.get(r-1, c-1) * i * r;
		fw.add(r, c, (lld)s*c);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] += -s * fw.get(r-1, c-1) * c;
		fw.add(r, c, (lld)s*i*r);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] += -s * fw.get(r-1, c-1) * r;
		fw.add(r, c, (lld)s*i*c);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] += -s * fw.get(r-1, c-1) * i;
		fw.add(r, c, (lld)s*r*c);
	}
	fw.clear();
	for (int i=1;i&amp;lt;=Q;i++) for (auto [r, c, k, s]: QR[i]){
		ans[k] -= -s * fw.get(r-1, c-1);
		fw.add(r, c, (lld)s*i*r*c);
	}
	for (int i=1;i&amp;lt;=K;i++) cout &amp;lt;&amp;lt; ans[i] &amp;lt;&amp;lt; '\n';
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>해법</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/144</guid>
      <comments>https://mwchun.tistory.com/144#entry144comment</comments>
      <pubDate>Mon, 29 Aug 2022 11:19:19 +0900</pubDate>
    </item>
    <item>
      <title>`22 현대모비스 알고리즘 경진대회 예선 후기</title>
      <link>https://mwchun.tistory.com/143</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;글을 쓰는 날짜 기준, 오늘(2022년 7월 1일) 오후 1시 30분부터 5시까지 약 3시간 30분 동안 `22 현대모비스 알고리즘 경진대회 예선에 참가했다. 문제 내용에 대한 언급은 서약 때문에 할 수 없어서 대회 환경에 대한 이야기를 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대회는 Goorm Devth(&lt;a href=&quot;https://devth.goorm.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://devth.goorm.io/&lt;/a&gt;)에서 진행됐다. 이 대회에서 가지는 몇 가지 특이 사항과 그로 인한 단점들, 응시하는 입장에서 주의해야 할 것을 적어보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;07/14 추가) 아래 언급된 것 중 일부분은 본선에서 개선이 되었다. 본선에서 개선된 부분들은 별도 표시(*)를 해두었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 실시간 채점 X (*)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 채점을 지원하지 않는다. 먼저, 다른 대회들의 진행 방식을 알아보자. 첫 번째로, 제일 많은 사람이 참가하는 것으로 알려진 Google Codejam은 실시간 채점을 반만 지원한다. Small set, large set이 있을 때, Small set만 결과를 알려주고 large set은 결과를 알려주지 않는다던지, 문제에 따라서 Large set까지 결과를 알려주는 경우도 있다. 두 번째로, KOI와 IOI 같은 경우 내가 학생이던 2010년~2012년을 기점으로 실시간 채점을 지원하기 시작했다. 그리고 다른 대부분의 대회에서 부분적으로라도 실시간 채점 결과를 알려준다. 실시간 채점이 도입된 이유는 여러 가지가 있지만, 가장 큰 이유로 코딩 실수로 인해 점수가 깎여 문제 풀이 능력이 높음에도 불구하고 낮은 점수를 받는 경우를 방지하기 위함이라고 개인적으로 생각한다. &lt;b&gt;즉, 실시간 채점은 최대한 응시자들의 실력을 여과 없이 보여줄 수 있는 수단이 된다는 것&lt;/b&gt;이다. 다만, 이 대회에서는 예제 데이터에 대해서만 채점 결과를 보여주고 나머지에 대해서는 대회 종료 3일 후에 결과를 공지해준다. &lt;b&gt;때문에 코딩을 하고 검수하는 과정을 거쳐서 점수가 나올 거라는 확신을 대회 중에 얻어야 한다. 다만, 아래 얘기할 몇 가지 특징 때문에 이도 거의 불가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 메모리 제한 언급 X, 스택 메모리 제한 언급 X&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제마다 시간 제한은 적혀있지만, 메모리 제한은 적혀있지 않았다. 그리고 스택 메모리 제한이 따로 있는지에 대해서도 언급이 없었다. 예선 대회 며칠 전 응시 환경 테스트로 나눠준 링크에 들어가서 테스트했을 때는, 스택 메모리 제한이 1MB 정도로 세팅되어 있는 것처럼 보였다. int argument 1개 있는 재귀 함수 깊이 10만은 잘 돌아가고 깊이 30만은 Segmantation fault 뜨는 정도였다. 이에 대해 공지가 없어서 1:1 문의하기로 질문한 결과 황당한 답변이 돌아왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;문의답변.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pOjdX/btrGcUUag71/YqINZWcOEq0d75ENcN9RxK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pOjdX/btrGcUUag71/YqINZWcOEq0d75ENcN9RxK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pOjdX/btrGcUUag71/YqINZWcOEq0d75ENcN9RxK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpOjdX%2FbtrGcUUag71%2FYqINZWcOEq0d75ENcN9RxK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;271&quot; height=&quot;414&quot; data-filename=&quot;문의답변.jpg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화의 맥락을 해치지 않는 선에서 중간에 불필요한 내용은 편집음을 먼저 말한다. 결론은, 메모리 제한을 포함하여 스택 메모리 제한이 따로 있는지 여부 등 아무것도 얘기해줄 수 없다는 것이다. 이들이 말하는 이유는 &lt;b&gt;응시자의 역량을 판단하는 주요 부분 중 하나이기 때문&lt;/b&gt;이다. 이게 응시자의 역량을 판단하는 주요 부분 중 하나인 것이 맞는지 틀리는지 여부를 떠나서 이로 인해 생길 수 있는 문제 상황에 대해 얘기해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로, 코딩하기에 따라서 메모리 사용량이 200MB, 400MB, 800MB 등 다양하게 나올 수 있는 문제가 있다고 하자. 메모리 제한이 얼마인지 모르기 때문에 풀이 과정 중 내가 생각할 수 있는 가장 적은 메모리 사용량을 가진 알고리즘을 구현해야 한다. 일반적으로 메모리 사용량이 적어지면, 실행 시간이 길어지는 trade-off가 있다. 시간제한은 문제에 명시되어 있지만, 가장 큰 데이터가 서버에서 얼만큼의 실행 시간을 가지는지 테스트해봐야 한다. &lt;b&gt;그런데 후술할 이유 때문에 이럴 수 있는 방법이 거의 없다시피 한다.&lt;/b&gt; 그리고 그렇게 해서 적은 메모리 사용량을 가지는 알고리즘을 구현했어도 서버에서 설정된 메모리 제한이 얼마인지 모르기 때문에 여전히 메모리 제한 초과일 수도 있다. 실시간 채점이 아니기 때문에 응시자는 대회 끝날 때까지 결과를 모른다. &lt;b&gt;다행히, 본선에서는 실시간 채점이 진행되어 이 부분에 대한 문제도 상당 부분 해결되었다. 코드를 제출해보고 메모리 사용량이 많다면 확인할 수 있기 때문에, 대회 중간 수정할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로, 스택 메모리 제한이 따로 설정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 백준에서 쉬운 문제들을 이것저것 풀기 시작하면서 다른 사람들이 짠 코드들을 많이 보기도 하는데, 지역 변수로 variable-length array를 사용하는 코드들도 종종 보이고, 이것 나름대로 장점이 있어 나도 가능하면 이런 방식으로 코딩하고 있다. 다만, 스택 메모리 제한이 따로 설정되어 있기 때문에 &lt;b&gt;variable-length array는 모조리 스택 메모리 영역에 올라간다.&lt;/b&gt; 그래서 크기가 100만인 int 배열 하나 잡으면 4MB로 스택 메모리 제한을 초과하게 된다! 이런 코딩 습관을 가지고 있는 사람들이 그 습관 그대로 코딩할 경우, 올바른 풀이를 가진 알고리즘일지라도 스택 메모리 초과로 낮은 점수를 받게 될 것이다. 다시 한번 말하지만, 실시간 채점이 아니기 때문에 대회가 끝나고 결과가 나올 때까지 이 사실을 모를 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어지는 얘기로 재귀 호출도 조심해서 작성해야 한다. 예를 들어, 1000x1000 크기의 2차원 격자판에서 Flood-fill을 한다면, bfs와 dfs 두 구현 모두 가능한 게 일반적이지만, 스택 메모리 제한이 있는 이런 환경에서는 dfs를 사용하면 최악의 경우 recursion depth가 100만이 되어, 스택 메모리 제한 초과로 segmentation fault를 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;마지막으로, 이 부분이 응시자들에게 공지되지 않았다.&lt;/b&gt; 나는 테스트 환경에서 테스트도 해보고, 1:1 문의하기를 통해 질문을 했기 때문에 저 정보들을 알고 의식하고 코딩을 할 수 있었는데, 과연 응시자 중 몇명이나 이와 같은 사항을 숙지하고 있었을까 의문이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 테스트 데이터 별로 점수 매기기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 많은 대회에서 ICPC 스타일로 AC or not을 하거나, IOI 스타일로 서브태스크를 둔다. 테스트케이스 별로 점수를 매기는 시스템은 정말 오랜만에 본다. 과거 KOI와 IOI는 테스트 데이터 별로 점수를 매겼다. 1)에서 이야기한 실시간 채점으로 넘어갈 때, 이 부분이 같이 바뀌었다. 서브태스크 제도를 채택하여, 서브태스크 안에 있는 데이터가 모두 맞아야지만 그 서브태스크에 해당하는 점수를 받을 수 있다. 1)에서 얘기한 이유와 마찬가지로 이 방법이 응시자들의 실력을 여과 없이 보여줄 수 있는 수단이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 예시로 테스트데이터 별로 점수 매기는 방법에 대한 문제점을 지적하겠다. 정해가 DP인 어려운 문제가 있다고 하자. 과거 KOI에서는 이런 문제를 못 풀 경우 다음과 같은 방법으로 최대한 많은 점수를 받기 위한 노력을 했다. N이 작으면 brute force로 풀고, N이 크면 그리디로 푼다. 이러한 방법은 문제에서 몇 점을 받을까? 결론은 테스트 데이터가 어떻게 구성되느냐에 따라 다르다. &lt;b&gt;문제 세터가 그리디를 저격하는 데이터를 몇 % 준비했는지에 따라 다르다.&lt;/b&gt; 만약, 그리디를 저격하는 데이터를 10%만 준비했다면, 이 코드는 90점 이상 받을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제는 서브태스크에서는 생기지 않는다. 만약, 그리디를 저격하는 데이터를 하나만 준비했더라도, N이 큰 서브태스크에 대해 0점을 받게 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점은 있지만 과거 KOI, IOI에서도 하는 방식이고 나름의 전략을 구현할 수 있어서 아쉬운 부분인 건 맞지만, 수긍할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 외부 IDE 사용 X, 오직 구름 에디터만 사용 가능, 구름 에디터 내부에서 복사/붙여넣기 불가능 (*)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 IDE 사용이 불가능하고, 오직 구름 에디터만 사용해야 한다. 그리고 구름 에디터 내부에서 복사/붙여넣기가 막혀있다. 이는 치팅을 방지하기 위함이다. 다만, 응시 환경을 매우 제한하는 조치로 소탐대실이다. 이로 인해 생기는 문제점은 매우 많다. 우선 구름 에디터 내부에서 복사/붙여넣기가 되지 않아서 일반적으로 코딩할 때 쓰는 복사/붙여넣기 조차 할 수 없다. 예를 들어, 한 코드 블록을 여러 개로 복제하고 싶을 때 그걸 일일이 손으로 다 쳐야 한다. main 함수에 있는 로직을 어떤 함수로 옮기고 싶다면, 마찬가지로 그 로직을 함수 안에서 직접 새로 타이핑해야 한다. 이 같은 조치는 응시자로 하여금 불필요한 시간을 들이게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 IDE 사용을 못하는 것도 많은 문제를 야기한다. 우리는 문제에서 시간제한이 몇 초인지 안다. 그러나 N = 100만처럼 큰 데이터에 대해 내가 작성한 알고리즘이 몇초 안에 돌아가는지 테스트할 수 있는 방법이 없다. 구름 IDE에서 테스트 데이터를 넣어볼 수 있는 기능이 있지만, N = 100만짜리 데이터를 만드려면 필연적으로 프로그램을 통해야 한다. 일반적인 대회 환경이라면 큰 데이터를 만드는 코드를 C++, Python 등으로 작성하여 넣어볼 수 있다. 그러나 우리는 구름 에디터 만을 사용해야 하기 때문에 큰 데이터를 직접 만들어서 넣어볼 수 없다. &lt;b&gt;즉, 내가 알고리즘을 작성해도 이게 시간 안에 동작하는지 테스트할 수 있는 방법이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5) 이상한 동점자 처리 기준&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 대회에서 동점자 처리 기준은 시험 종료를 얼마나 빨리 눌렀느냐이다. 보통 만점을 받을 것 같다는 확신이 있다면, 시험 중간에 시험을 종료할 것이다. 그러나 이런 동점자 처리 기준 때문에 만점을 받지 못해도, 본인이 남은 시간 안에 점수를 올릴 것 같다는 희망이 없으면 중간에 시험 종료를 하는 것이 등수에 유리한 조건이다. 그리고 여기서도 &lt;b&gt;가장 큰 문제는 실시간 채점이 없기 때문에 현재 내 점수를 몰라 쉬운 문제에서 실수를 했는지 모른다는 것&lt;/b&gt;이다. 이런 상황이라면 보통 시간을 더 써서, 쉬운 문제 코드를 검증할텐데 동점자 처리 기준 때문에 취사선택을 잘해야 하는 웃픈 상황이 벌어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본선에서는 실시간 채점을 지원했지만, 여전히 현재 동점차 처리 기준의 문제점이 남아있다.&lt;/b&gt; 예를 들어, 마지막 문제를 풀지 못했고 남은 대회 시간 안에 정해를 풀지 못할 것 같으면, 대회 시간 1시간이 남은 시점에서 시험 종료를 하는 것이 등수로 유리한 선택이 될 수 있다는 것이다. 이는 응시자에게 매우 가혹하게 다가온다. 과연 &lt;b&gt;같은 점수를 받은 서로 다른 두 사람의 우열(등수)을 가리는데, 일찍 포기(시험 종료)한 사람이 더 잘했다고 할 수 있는가?&lt;/b&gt; 대회를 오래 준비해온 입장에서 생각해보았을 때, 일찍 포기한 사람이 더 높은 등수를 받는 것이 매우 이질적으로 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 가장 선호하는 동점자 처리 기준은 NYPC에서 사용하는, 제출 횟수와 상관 없이 그 점수에 도달한 시각이 가장 빠른 순서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6) 미숙한 재채점 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대회 진행 도중 어떤 문제에 대해 수정 사항이 있었다. 문제 본문이나 예제 입출력이 수정된 것이 아닌, 응시자가 모르는 어떤 무언가가 수정되었다고 한다. 이 수정으로 인해, 그 문제를 이미 제출한 사람은 다시 제출을 해야 한다고 공지가 되었다. 길지 않은 3시간 30분 정도의 대회 시험 환경에서 상당히 여러 차례 공지가 있었다. 응시자 화면에서 공지가 잘 노출이 되는 곳은 가장 마지막으로 온 공지가 자동으로 노출되기 때문에 &lt;b&gt;노트를 바라보며 문제 풀이에 집중한 응시자는 이 공지를 놓칠 수도 있다.&lt;/b&gt; 만약, 공지를 놓쳐서 그 문제를 다시 제출하지 않은 응시자가 받는 불이익은 어떻게 보상받는가? 대회를 진행하다 보면, 채점 데이터에 당연히 실수는 있을 수 있다. 그것을 비판하는 것이 아니다. 채점 데이터에 수정이 있더라도 응시자가 대회에 집중하는 흐름을 해치지 않도록 자연스럽게 재채점이 진행되어야 한다. 예를 들어, 이미 한번 재채점 공지를 본 응시자들은 대회를 진행하면서도 자신이 어떤 공지를 놓쳐 불이익을 받지 않을까 의식을 계속해야 한다. 그리고 가장 큰 문제점은 &lt;b&gt;5)의 동점자 처리 기준 때문에 그 공지를 보기 전에 시험 종료한 응시자가 있을 수 있다!&lt;/b&gt; 그러면 그 응시자는 공지도 못 보고 다시 제출도 못하는데, 이와 같은 상황이 있어서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*) 결론&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 2005년에 처음 KOI에 참가한 이후로 과도기도 직접 겪었고 아주 다양한 대회 환경에 참여, 세팅을 해봤었다. 그런데 이렇게 제한적이고 불편한 대회 환경을 경험해본 적이 없다. 실시간 채점이 아니기 때문에 반드시 필요한 랜덤 데이터 생성기를 작성하고 brute force 알고리즘과 답을 비교하여 검증하는 것을 할 수 없다. 큰 데이터를 만들어서 실행 시간을 측정해보는 테스트도 할 수 없다. 작은 반례 데이터 정도는 손으로 작성해서 넣어보고 답이 잘 나오는지 확인할 수는 있다. 재귀 호출 깊이가 깊어질 것 같으면, 재귀 호출을 이용한 풀이를 할 수가 없다. 혹은 직접 힙 메모리 영역에 스택을 구현하여 재귀 호출을 비재귀적으로 구현해야 한다. 메모리 제한이 공개되지 않았고, 실시간 채점이 아니기 때문에 알고리즘이 틀리지 않았더라도, 불필요하게 메모리를 많이 쓰면 500점 받을 사람의 결과가 300점이 되어있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;과연 이와 같은 대회 환경이 응시자의 실력을 여과 없이 보여줄 수 있는 대회 환경이라고 할 수 있겠는가?&lt;/b&gt; 알고리즘 경진대회이기 때문에 자신이 작성한 알고리즘을 테스트해볼 수 있어야 하고, 틀린 게 스스로 확인된다면 디버깅도 해볼 수 있어야 한다. 지금 환경에서는 그것이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경은 대회 마다 다르다. 그것이 그 대회의 독특한 특징이 될 수도 있다. 예를 들어, KOI는 (내가 생각했을 때) 제출을 적게 하여 높은 점수를 받는 것이  실력을 판가름하는 데에 중요하다고 생각하여 제출 횟수를 동점자 처리 기준에 두었을 것이다. 과연 현대모비스는 일찍 포기(시험 종료) 하는 것을 실력을 판가름하는 데에 중요하다고 생각하여 이러한 동점자 처리 기준을 두었을지 의문이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IOI의 과도기인 IOI 2010, IOI 2011을 직접 경험한 입장에서 대회 환경이 성적에 미치는 영향이 매우 크다고 말할 수 있다. 물론, 잘하는 사람들은 환경이 어떻게 변하든 간에 좋은 성적을 받는다. tourist는 대회 환경이 대폭 바뀐 IOI 2010와 IOI 2011에서도 이전처럼 우승했다. 이 대회에서도 환경이 얼마나 불편하든 잘하는 소수의 사람은 여전히 좋은 성적을 받을 것이다. 다만, 그렇지 않은 사람도 적지 않다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝으로, 대회를 망쳐서 감정적으로 작성하는 글은 아니다. 성적은 7/4 오후 2시 발표라 내 점수가 몇 점인지, 등수가 몇 등인지 아직 모른다. 다만, 지금까지 여러 대회에 참여했고 여러 대회를 준비한 입장에서 현재 환경에 대한 아쉬움을 매우 많이 느끼기 때문에 개선에 조금의 도움이라도 되길 바라는 마음으로 글을 적는다. 이 글을 어떤 사람들이 읽을지, 앞으로 현대모비스 알고리즘 경진대회가 어떻게 변할지 모르겠지만, 현재 환경은 명백히 최악이며, 개선할 여지가 매우 매우 많다. 알고리즘 대회를 아끼고 사랑하는 사람으로서 많은 사람들이 불편한 환경 때문에 대회를 즐기는 데에 방해가 되지 않길 바라는 마음이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7/10 추가)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본선 대회는 1) 실시간 채점이 아니었던 점과 4) 외부 IDE 사용이 불가능했던 점이 개선되었다. 이로 인해 응시자는 자신의 IDE를 사용하여, 시간초과 나는 코드를 짜서 랜덤데이터를 넣어 자신의 프로그램을 검증할 수 있고, 큰 데이터도 문제 없이 생성하여 테스트해볼 수 있다. 실시간 채점에서도 재미있는 부분이 있는데, 테스트케이스에 대한 결과는 알려주지 않고, 모든 테스트케이스를 통과한 경우 정답을 받았다고 알려준다. 또한, 모든 테스트케이스를 통과하지 않았더라도 각 테스트케이스에서 메모리 사용량, 실행 시간 등을 알려준다. 어떤 테스트케이스에 대해 시간초과인 경우 모든 칸이 빈칸으로 나오기 때문에, 시간초과 받았는지도 알 수 있다. 제출 횟수에 따른 패널티가 없다는 점을 이용하여, 중간중간 코드를 제출하여 준비된 채점 데이터에서 내 코드가 시간초과 나는지 쉽게 확인할 수 있다. 예를 들어, 문자열 N개가 주어진 문제에서 문자열을 $O(N \lg N)$으로 정렬하는 게 시간초과가 나는지 제출하여 쉽게 확인할 수 있다. 이러한 개선으로 인해, 응시자가 불필요하게 느끼는 불편은 대부분 해소되었다고 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 메모리 제한 언급이 없는 점과 스택 메모리 제한이 작게 걸려있는 점, 3) 테스트케이스 별로 점수 매기는 점, 5) 괴랄한 동점자 처리 기준 등이 남아있긴 하지만, 종합적으로 생각했을 때, 남아있는 사항들은 현대 모비스 대회만의 특징으로 자리 잡을 수 있을 것이라 생각한다. 예상치 못한 빠른 피드백 반영에 조금 놀랐다. 내년에 열리는 대회는 어떤 플랫폼에서 어떤 특징들을 가지고 진행되는지 알 수 없지만, 만약 같은 환경으로 진행된다면 이 글에서 언급된 내용들을 토대로 대회에 알맞은 전략을 구성할 수 있을 것 같다.&lt;/p&gt;</description>
      <category>잡담</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/143</guid>
      <comments>https://mwchun.tistory.com/143#entry143comment</comments>
      <pubDate>Fri, 1 Jul 2022 17:36:06 +0900</pubDate>
    </item>
    <item>
      <title>Google Code Jam 2022 Round 2 풀이 및 후기</title>
      <link>https://mwchun.tistory.com/142</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 풀이 및 후기 글을 적는다. Google Code Jam은 매년 꾸준히 참가해왔다. 그동안 PS 공부할 시간적 여유가 없어서, 참가만 해왔었고, 다행히 최근에는 육아휴직으로 시간이 생겨서 밀린 PS 공부를 했다. 주로 최근에 well-known이 된 알고리즘/자료구조들을 공부하고 익히는 시간이었다. 다만, 최근 Round 2 성적이 최근에 공부한 것을 감안했을 때 아쉬움이 많이 남는 결과라 현재 심경과 풀이를 정리할 겸 포스팅을 시작한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;A. Spiraling Into Control&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spiral 모양의 $N \times N$ 크기의 행렬이 있을 때, 원하는 만큼 shortcut을 놓아 정확히 $K$ 번의 이동으로 출발점 $1$에서 도착점 $N^2$으로 가는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Shortcut이 하나도 없을 때 이동하는 횟수는 $N^2-1$ 번이다. 즉, shortcut을 적절히 놓아 이동 횟수를 $N^2-1-K$ 만큼 줄이는 문제가 된다. 한 가지 관찰이 필요하다. 각 칸에 놓을 수 있는 shortcut은 최대 1개다. 어떤 칸에 shortcut을 놓을 수 있다면, 그 shortcut으로 인해 줄어드는 이동 횟수를 생각해보자. 다음 그림은 $5 \times 5$ 크기의 행렬에서 가능한 shortcut들이 모두 표시되어 있고, shortcut 마다 줄일 수 있는 이동 횟수 값도 같이 적혀있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fig1.jpg&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DVfPq/btrCbnkriCc/7ns6qLrHXGHHzUPxBU5Ma1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DVfPq/btrCbnkriCc/7ns6qLrHXGHHzUPxBU5Ma1/img.jpg&quot; data-alt=&quot;&amp;amp;lt;그림 1&amp;amp;gt; 5x5 격자판에 가능한 shortcut&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DVfPq/btrCbnkriCc/7ns6qLrHXGHHzUPxBU5Ma1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDVfPq%2FbtrCbnkriCc%2F7ns6qLrHXGHHzUPxBU5Ma1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;460&quot; data-filename=&quot;fig1.jpg&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 1&amp;gt; 5x5 격자판에 가능한 shortcut&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N \times N$ 크기의 행렬에서 Shortcut은 이동 횟수를 $4N-6$ 줄이는 것부터 $2$ 만큼 줄이는 것까지 있다. 그리고 이동 횟수를 $k$ 만큼 줄이는 shortcut을 이용할 경우, 다음으로 이용 가능한 shortcut은 이동 횟수를 $k-8$부터 줄일 수 있다. 예를 들어, &amp;lt;그림 1&amp;gt;에서 이동 횟수를 14 만큼 줄이는 shortcut을 이용할 경우, 이동 횟수를 12, 10, 8 만큼 줄이는 shortcut은 이용할 수 없고, 이동 횟수를 6, 4, 2 만큼 줄이는 shortcut을 이용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반으로, 줄여야 하는 이동 횟수 $X = N^2-1-K$가 있을 때, $2$ 이상 $4N-6$ 이하인 짝수 중 고른 수들의 차이가 8이상이 되도록 적절히 수를 골라서 합이 $X$가 되도록 만드는 문제가 되었다. 만약, $X$가 홀수라면 답은 IMPOSSIBLE이고, $X$가 짝수라면 $4N-6$부터 시작하여 그리디 하게 큰 수부터 고르면 된다. 마지막으로 실제 이용하는 shortcut은 가장 앞에 오는 shortcut만 이용하면 항상 가능하다는 것을 알 수 있다. 예를 들어, 위 그림에서 14를 골랐다면 (1, 16)인 shortcut을 항상 이용하고, 12를 골랐다면 (6, 19), 10은 (10, 21), 8은 (14, 23), 6은 (17, 24), 4는 (20,25), 2는 (22, 25)를 이용하면 된다. 시간복잡도는 $O(N)$이 된다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

bool solve(){
	int N, K; scanf(&quot;%d%d&quot;, &amp;amp;N, &amp;amp;K);
	int X = N*N-1-K, Y = 4*N-6;
	if (X%2 == 1) return 0;
	vector&amp;lt;int&amp;gt; arr;
	for (int i=Y;i&amp;gt;0;){
		if (X &amp;gt;= i){
			arr.push_back(i);
			X -= i, i -= 8;
		}
		else i -= 2;
	}
	if (X &amp;gt; 0) return 0;
	pair&amp;lt;int, int&amp;gt; shortcuts[Y+1];
	int cur = 1, d = N*2;
	for (int i=Y;i&amp;gt;0;i-=2,d--){
		shortcuts[i] = make_pair(cur, cur+i+1);
		cur += d/2;
	}
	printf(&quot;%lu\n&quot;, size(arr));
	for (int v: arr){
		auto&amp;amp; p = shortcuts[v];
		printf(&quot;%d %d\n&quot;, p.first, p.second);
	}
	return 1;
}

int main(){
	int T, ts = 0;
	for (scanf(&quot;%d&quot;, &amp;amp;T);T--;){
		printf(&quot;Case #%d: &quot;, ++ts);
		if (!solve()) puts(&quot;IMPOSSIBLE&quot;);
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;B. Pixelated Circle&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 방식으로 지름이 $R$인 원을 내부를 색칠하며 그린다. 두 방식에 대한 자세한 동작 과정은 본문에 의사 코드로 표현되어 있다. 한 방법은 draw_circle_filled(R)이고, 다른 방법은 draw_circle_filled_wrong(R)이다. 편의상 순서대로 옳은 방법, 틀린 방법이라고 표현하자. R이 주어졌을 때, 옳은 방법이 색칠하는 픽셀 수와 틀린 방법이 색칠하는 픽셀 수의 차이를 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 실제로 옳은 방법과 틀린 방법을 $O(R^2)$ 시간복잡도로 구현하는 코드를 작성한 뒤, 몇 가지 특징들을 찾는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징 1. 틀린 방법이 색칠하는 모든 픽셀은 옳은 방법 또한 색칠한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 우리는 옳은 방법은 색칠하지만 틀린 방법은 색칠하지 못하는 픽셀을 특정하면 된다. 또한, 답은 (옳은 방법이 색칠하는 픽셀 수)-(틀린 방법이 색칠하는 픽셀 수)가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징 2. x = 0 혹은 y = 0인 픽셀에 대해 옳은 방법이 색칠하는 모든 픽셀을 틀린 방법 또한 색칠한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징 3. 1사분면에 대해 답을 구하고 *4를 하면 답이 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 방법 모두 색칠하는 픽셀이 x축 대칭, y축 대칭이 되므로 1 사분면에 대해 답을 구하면 전체 답을 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징 4. 1사분면에 색칠되는 픽셀들은 $y = x$ 직선을 기준으로 대칭이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 색칠 되는 픽셀 중 $x \leq y$인 것의 개수를 구하면, 1 사분면에 색칠되는 픽셀의 수를 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징 5. 틀린 방법이 호출하는 함수 draw_circle_perimeter(r)은 서로 다른 r1, r2에 대해 색칠하는 픽셀이 항상 겹치지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 0 이상 $R$인 $r$에 대해 draw_circle_perimeter(r)이 색칠하는 픽셀 수를 모두 더하면 틀린 방법이 색칠하는 픽셀 수가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징 6. draw_circle_perimeter(r)이 1사분면에 색칠하는 픽셀 중 $x \le y$인 픽셀들은 모두 $x$ 좌표가 서로 다르고, 한 $x$ 좌표에 대해 정확히 한 픽셀이 색칠된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하여 draw_circle_perimeter(r)이 1사분면에 색칠하는 픽셀의 수를 빠른 시간 안에 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 몇 가지는 수학적으로 증명이 비교적 간단하게 가능하고, 몇 가지는 수학적 증명보다 관찰과 코드를 통해 맞는지 확인하는 것이 좋다. 이 중 특징 4, 5, 6은 Test set 2를 푸는데 반드시 필요하다. 옳은 방법이 색칠하는 픽셀 개수를 $O(R)$ 혹은 $O(R \lg R)$ 시간복잡도로 세는 것은 간단하니 생략하고, 틀린 방법이 색칠하는 픽셀 개수를 구하는 방법에 대해 조금 짚어보겠다. 즉, 0 이상 $R$ 이하인 $r$에 대해 draw_circle_perimeter(r)가 색칠하는 픽셀 수를 구하면 되는데, 특징 6을 이용할 것이다. $x \leq y$를 만족하는 가장 큰 $x$를 구하면 색칠된 픽셀의 개수를 구할 수 있다. 이 가장 큰 $x$는 $r \cos(45^\circ)$ 근처가 된다. 그러나 이를 이분 탐색으로 $O(\lg r)$ 시간에 구해도 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨부된 코드는 소수점 연산등을 크게 고민하지 않아도 되는 $O(R \lg R)$ 시간복잡도를 가지는 방법이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

void solve(){
	int R; scanf(&quot;%d&quot;, &amp;amp;R);
	lld correct = 0, wrong = 1;
	for (int r=1;r&amp;lt;=R;r++){
		int s = 0, e = r, xt, yt;
		while (s &amp;lt;= e){
			int x = (s+e) &amp;gt;&amp;gt; 1;
			int y = round(sqrt((lld)r*r-(lld)x*x));
			if (x &amp;lt;= y) s = x+1, xt = x, yt = y;
			else e = x-1;
		}
		int c = xt*2;
		if (xt &amp;gt; 0 &amp;amp;&amp;amp; xt == yt) c--;
		c += 2;
		wrong += c;
	}
	for (int x=0;x&amp;lt;=R;x++){
		int s = 0, e = R, y;
		while (s &amp;lt;= e){
			int m = (s+e) &amp;gt;&amp;gt; 1;
			if (round(sqrt((lld)x*x+(lld)m*m)) &amp;lt;= R) s = m+1, y = m;
			else e = m-1;
		}
		correct += y+1;
	}
	printf(&quot;%lld\n&quot;, (correct-wrong)*4);
}

int main(){
	int T, ts = 0;
	for (scanf(&quot;%d&quot;, &amp;amp;T);T--;){
		printf(&quot;Case #%d: &quot;, ++ts);
		solve();
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;C. Saving the Jelly&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N$ 명의 학생이 2차원 평면 위에 있고, $N+1$ 개의 사탕이 2차원 평면 위에 있다. 코치는 자신이 원하는 순서대로 학생을 호명하는데, 호명된 학생은 자신과 가장 가까운 사탕을 가지고 집으로 돌아간다. 만약, 가장 가까운 사탕이 여러 가지인 경우 코치는 그중 학생이 가져갈 사탕을 고른다. $N+1$ 개의 사탕 중 사탕 $1$은 코치 자신이 가지고 싶은 사탕이다. 이때, 적절한 순서로 학생들을 호명하여 아무도 사탕 $1$을 가져가지 못하게 하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 답이 존재하는 필요 조건을 생각해보자. 모든 학생은 사탕 $1$보다 거리가 가까운 사탕 혹은 사탕 $1$과 거리가 같은 사탕을 가져가야 한다. 때문에 이분 매칭으로 왼쪽은 $N$ 명의 학생, 오른쪽은 사탕 $1$을 제외한 $N$ 개의 사탕을 놓고, 만약 학생 $i$와 사탕 $1$과의 거리가 사탕 $j$과의 거리보다 같거나 멀면 $i$에서 $j$로 가는 간선을 만들어준다. 이때, 답이 존재하기 위한 필요조건은 완전 매칭(perfect matching)이 존재하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지만 생각했을 때, 완전 매칭이 존재하는 것은 필요 조건이지 충분조건이 아니다. 그런데, 임의의 완전 매칭이 있을 때 답을 항상 구할 수 있음을 보이면서 이 조건이 필요충분조건임을 같이 증명할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 임의의 완전매칭을 구한다. 현재 상황에서, 남은 사탕 중에 어떤 학생 $i$와 제일 가까운 사탕이 $j$이고 완전 매칭에서 $i$와 $j$가 매칭 되어있다면, 학생 $i$가 호명되어 사탕 $j$를 가져가면 된다. 이제 이러한 학생이 한 명도 없을 때 완전 매칭을 적절히 바꾸어 이런 학생이 생기도록 항상 만들 수 있음을 보이겠다. 어떤 학생 $s_1$과 가장 가까운 사탕은 $c_1$이고, 완전 매칭에서 $c_1$은 $s_2$와 매칭 되어 있다. 마찬가지로 $s_2$와 가장 가까운 사탕은 $c_2$고, 완전 매칭에서 $c_2$은 $s_3$과 매칭 되어 있다. $s_3$와 가장 가까운 사탕은 $c_3$이고, 완전 매칭에서 $c_3$은 $s_1$과 매칭 되어 있다. 이러한 사이클을 우리는 항상 찾을 수 있고, 이러한 사이클을 찾은 경우, $s_1$을 $c_1$과, $s_2$는 $c_2$와, $s_3$은 $c_3$과 재매칭 시킬 수 있다. 아래 &amp;lt;그림 2&amp;gt;를 참고하자. 즉, 임의의 사이클을 찾은 뒤 이러한 재매칭 작업을 통해 어떤 학생 $i$와 가장 가까운 사탕 $j$를 매칭 시킬 수 있다. 이 과정을 반복하여 문제에서 요구하는 답을 항상 구할 수 있다. 동시에 위에서 말한 필요조건이 필요충분조건임을 증명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fig2.jpg&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDoYuz/btrCcEGrHOv/2gP8RSeb9sIQErsaY5aRHK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDoYuz/btrCcEGrHOv/2gP8RSeb9sIQErsaY5aRHK/img.jpg&quot; data-alt=&quot;&amp;amp;lt;그림 2&amp;amp;gt; 싸이클의 재매칭 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDoYuz/btrCcEGrHOv/2gP8RSeb9sIQErsaY5aRHK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDoYuz%2FbtrCcEGrHOv%2F2gP8RSeb9sIQErsaY5aRHK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;301&quot; data-filename=&quot;fig2.jpg&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 2&amp;gt; 싸이클의 재매칭 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식으로 문제를 해결하면 완전매칭을 Hopcroft-Karp로 구하는데 $O(N^2\sqrt{N})$ 시간이 걸리고, 구한 완전 매칭에서 문제에서 요구하는 답을 구하는데 $O(N^2)$ 시간이 걸린다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

constexpr int MAXN = 1004, inf = 1e9;
int N;
struct Point{
	int x, y;
} A[MAXN], B[MAXN];
vector&amp;lt;pair&amp;lt;lld, int&amp;gt;&amp;gt; D[MAXN];

lld get_dist(const Point&amp;amp; a, const Point&amp;amp; b){
	return (lld)(a.x-b.x)*(a.x-b.x)+(lld)(a.y-b.y)*(a.y-b.y);
}

int match[MAXN], dist[MAXN];
bool used[MAXN];

void bfs(){
	queue&amp;lt;int&amp;gt; que;
	for (int i=1;i&amp;lt;=N;i++) dist[i] = inf;
	for (int i=1;i&amp;lt;=N;i++) if (!used[i]) dist[i] = 0, que.push(i);
	while (!que.empty()){
		int q = que.front(); que.pop();
		for (auto [_, t]: D[q]){
			int m = match[t];
			if (m &amp;amp;&amp;amp; dist[m] == inf) dist[m] = dist[q]+1, que.push(m);
		}
	}
}

bool dfs(int n){
	for (auto [_, t]: D[n]){
		int m = match[t];
		if (!m || dist[m] == dist[n]+1 &amp;amp;&amp;amp; dfs(m)){
			used[n] = 1; match[t] = n;
			return 1;
		}
	}
	dist[n] = inf;
	return 0;
}

void solve(){
	scanf(&quot;%d&quot;, &amp;amp;N);
	for (int i=1;i&amp;lt;=N;i++) scanf(&quot;%d%d&quot;, &amp;amp;A[i].x, &amp;amp;A[i].y);
	for (int i=0;i&amp;lt;=N;i++) scanf(&quot;%d%d&quot;, &amp;amp;B[i].x, &amp;amp;B[i].y);
	for (int i=1;i&amp;lt;=N;i++){
		D[i].clear();
		lld d0 = get_dist(A[i], B[0]);
		for (int j=1;j&amp;lt;=N;j++){
			lld d = get_dist(A[i], B[j]);
			if (d &amp;lt;= d0)
				D[i].emplace_back(d, j);
		}
		sort(begin(D[i]), end(D[i]), greater&amp;lt;pair&amp;lt;lld, int&amp;gt;&amp;gt;());
	}
	for (int i=1;i&amp;lt;=N;i++) used[i] = match[i] = 0;
	int matched = 0;
	for (;;){
		bfs();
		int flow = 0;
		for (int i=1;i&amp;lt;=N;i++) if (!used[i] &amp;amp;&amp;amp; dfs(i)) flow++;
		if (!flow) break;
		matched += flow;
	}
	if (matched &amp;lt; N){ puts(&quot;IMPOSSIBLE&quot;); return; }

	puts(&quot;POSSIBLE&quot;);
	vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; ans;
	vector&amp;lt;bool&amp;gt; vis1(N+1), vis2(N+1);
	auto get = [&amp;amp;](int i){
		while (vis2[D[i].back().second]) D[i].pop_back();
		return D[i].back().second;
	};
	while (size(ans) &amp;lt; N){
		for (int i=1;i&amp;lt;=N;i++) if (!vis1[i]){
			int j = get(i);
			if (match[j] == i){
				ans.emplace_back(i, j);
				vis1[i] = vis2[j] = 1;
			}
		}
		for (int i=1;i&amp;lt;=N;i++) if (!vis1[i]){
			vector&amp;lt;bool&amp;gt; chk(N+1);
			vector&amp;lt;int&amp;gt; path;
			for (int j=i;;j=match[get(j)]){
				if (chk[j]){
					for (;;){
						int k = path.back(); path.pop_back();
						match[get(k)] = k;
						if (k == j) break;
					}
					break;
				}
				chk[j] = 1; path.push_back(j);
			}
			break;
		}
	}
	for (auto [p, q]: ans) printf(&quot;%d %d\n&quot;, p, q+1);
}

int main(){
	int T, ts = 0;
	for (scanf(&quot;%d&quot;, &amp;amp;T);T--;){
		printf(&quot;Case #%d: &quot;, ++ts);
		solve();
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;D. I, O Bot&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일차원 좌표 공간에 로봇이 하나 있고, 물건이 $N$ 개 있다. 물건은 종류 $0$ 또는 종류 $1$이다. 로봇에는 종류 $0$인 물건 한 개를 담을 수 있는 공간, 종류 $1$인 물건 한 개를 담을 수 있는 공간이 있다. 로봇이 왼쪽 혹은 오른쪽으로 한 칸 움직일 때 필요한 비용은 $1$이고, 현재 위치에 있는 물건을 공간에 담는 비용은 $0$이다. 다만, 물건을 담을 공간이 이미 차있는 경우 물건을 다른 종류로 바꿀 수 있는데, 물건의 종류를 바꾸는 비용은 $C$다. 로봇은 좌표 $0$에 도달했을 때 담은 물건을 내려놓을 수 있는데, 내려놓는 비용은 $0$이다. 로봇이 좌표 $0$에서 시작하여, 모든 물건을 좌표 $0$에 옮길 때 필요한 최소 비용을 구하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물건의 좌표는 양수와 음수 모두 가능하다. 양수 좌표에 있는 물건들만 있다고 생각하고 문제를 해결하고, 음수 좌표에 있는 물건들만 있다고 생각하고 문제를 해결하여 둘의 결과값을 더하면 전체 결과가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test set 1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$O(N^2)$ 시간복잡도를 가지는 DP로 해결할 수 있다. 다음과 같이 DP 배열을 정의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D[i][j] = 종류 $0$인 물건 i개, 종류 $1$인 물건 j개를 원점에 가져다 놓을 때 필요한 최소 비용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, 종류 $0$인 물건 i개란, 좌표가 순으로 정렬했을 때 앞에 오는 물건 i개를 의미한다. 즉, 같은 종류의 물건이 여러 개 있는 경우 원점과 가까운 물건을 먼저 옮기는 것이 더 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다음 상황으로 가능한 것은 크게 3가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 물건을 1개만 담아 원점으로 돌아오는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원점에서 출발하여 한 물건을 담고 다시 원점으로 돌아와 내려놓는다. 이때, 담는 물건은 종류 $0$ 남은 물건 중 가장 원점과 가까운 물건이거나 종류 $1$ 남은 물건 중 가장 원점과 가까운 물건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 종류 $0$인 물건 하나, 종류 $1$인 물건 하나를 담아 원점으로 돌아오는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종류 $0$인 남은 물건 중 가장 원점과 가까운 물건, 종류 $1인 남은 물건 중 가장 원점과 가까운 물건을 담고 원점으로 돌아와 내려놓는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 같은 종류의 물건을 2개 담아 원점으로 돌아오는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반성을 잃지 않고 종류 $0$인 물건 2개를 담고 돌아온다고 하자. 종류 $0$인 남은 물건 중 원점과 가장 가까운 물건 2개를 담는데, 그중 하나는 비용 $C$를 들여 종류 $1$로 바꾸고 담는다. 그리고 원점으로 돌아와 내려놓는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 경우 각각에 대하여 뿌려주는 형태의 DP를 통해 쉽게 문제를 해결할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;
constexpr lld inf = 1e18;

lld solve(vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;amp; arr, int C){
	auto put_min = [](auto&amp;amp; a, auto b){
		a = min(a, b);
	};
	sort(begin(arr), end(arr));
	vector&amp;lt;int&amp;gt; A, B;
	for (auto [x, s]: arr){
		if (s == 0) A.push_back(x);
		else B.push_back(x);
	}
	int n = size(A), m = size(B);
	lld D[n+1][m+1];
	for (int i=0;i&amp;lt;=n;i++) for (int j=0;j&amp;lt;=m;j++) D[i][j] = inf;
	D[0][0] = 0;
	for (int i=0;i&amp;lt;=n;i++) for (int j=0;j&amp;lt;=m;j++) if (D[i][j] &amp;lt; inf){
		lld v = D[i][j];
		if (i+1 &amp;lt;= n) put_min(D[i+1][j], v+A[i]*2);
		if (j+1 &amp;lt;= m) put_min(D[i][j+1], v+B[j]*2);
		if (i+1 &amp;lt;= n &amp;amp;&amp;amp; j+1 &amp;lt;= m) put_min(D[i+1][j+1], v+max(A[i], B[j])*2);
		if (i+2 &amp;lt;= n) put_min(D[i+2][j], v+A[i+1]*2+C);
		if (j+2 &amp;lt;= m) put_min(D[i][j+2], v+B[j+1]*2+C);
	}
	return D[n][m];
}

void solve(){
	int N, C;
	scanf(&quot;%d%d&quot;, &amp;amp;N, &amp;amp;C);
	vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; A, B;
	for (int i=0;i&amp;lt;N;i++){
		int x, s; scanf(&quot;%d%d&quot;, &amp;amp;x, &amp;amp;s);
		if (x &amp;gt; 0) A.emplace_back(x, s);
		else B.emplace_back(-x, s);
	}
	lld ans = solve(A, C)+solve(B, C);
	printf(&quot;%lld\n&quot;, ans);
}

int main(){
	int T, ts = 0;
	for (scanf(&quot;%d&quot;, &amp;amp;T);T--;){
		printf(&quot;Case #%d: &quot;, ++ts);
		solve();
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Test set 2&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해서 조금 다른 DP를 써야 한다. 물건들이 종류 상관없이&amp;nbsp;위치를 기준으로 정렬되어 있다고 가정하고, 다음과 같이 DP 배열을 정의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D[i] = 왼쪽 $i$ 개 물건을 원점에 가져다 놓을 때 필요한 최소 비용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 조건으로는 D[0] = 0, D[1] = 2*X[1]이 된다. 이제 D[i]를 계산하는 점화식을 구할 차례다. 일반성을 잃지 않고 $i$ 번째 물건의 종류는 $0$이라고 가정하자. 어떤 물건 둘을 한 번의 왕복에서 같이 로봇에 담아 원점으로 옮길 경우 두 물건을 매칭 된다고 표현하자. 이제 다음과 같이 경우를 크게 세 가지로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) $i$ 번째 물건이 매칭되지 않는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 최소 비용은 D[i-1]+2*X[i]가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) $i$ 번째 물건이 매칭되는데, $i-1$ 번째 물건이 종류 $1$인 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 $i$ 번째 물건은 $i-1$ 번째 물건과 매칭 되는 것이 항상 좋다. 즉, 최소 비용은 D[i-2]+2*X[i]가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) $i$ 번째 물건이 매칭되는데, $i-1$ 번째 물건이 종류 $0$인 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-1) $i$ 번째 물건이 종류 $0$인 물건과 매칭 되는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우도 2)와 마찬가지로 $i$ 번째 물건은 $i-1$ 번째 물건과 매칭 되는 것이 항상 좋다. 즉, 최소 비용은 D[i-2]+2*X[i]+C가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-2) $i$ 번째 물건이 종류 $1$인 물건과 매칭되는 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 생각하기 가장 까다로운 경우다. $i$ 번째 물건과, $i-1$ 번째 물건이 모두 종류 $0$이면서, $i$ 번째 물건은 종류 $1$인 물건과 매칭 되는 경우다. $i$ 번째 물건보다 왼쪽에 있으면서 가장 가까운 종류 $1$인 물건을 $j$ 번째 물건이라고 하자. 즉, $j+1, j+2, \cdots, i$ 번째 물건 모두 종류 $0$이다. 이때, 몇 가지 관찰이 필요한데 바로 왼쪽 $i$ 개 물건을 원점에 가져다 놓는 최적의 매칭이 존재하고, 최적의 매칭에서 $i$ 번째 물건이 종류 $1$인 물건과 매치가 되었다면, $i$ 번째 물건은 $j$ 번째 물건과 매칭이 되어야 하고, $i-1$ 번째 물건 또한 종류 $1$인 물건과 매치되어야 한다. 두 가지 모두 귀류법으로 어렵지 않게 증명 가능하다. 이제 이 명제를 연쇄적으로 적용할 수가 있는데, $i$ 번째 물건은 $j$ 번째 물건과 매칭 시키고, $i-1$ 번째 물건은 남은 종류 $1$인 물건 중 가장 오른쪽에 있는 물건과 매칭이 된다. 이 상황을 그림으로 나타내면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fig3.jpg&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NflYe/btrB9tF5ZJ2/ApntNSTzuw8EqklVXWX8nk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NflYe/btrB9tF5ZJ2/ApntNSTzuw8EqklVXWX8nk/img.jpg&quot; data-alt=&quot;&amp;amp;lt;그림 3&amp;amp;gt; i 번째 물건이 종류 1인 물건과 매칭되는 경우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NflYe/btrB9tF5ZJ2/ApntNSTzuw8EqklVXWX8nk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNflYe%2FbtrB9tF5ZJ2%2FApntNSTzuw8EqklVXWX8nk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1894&quot; height=&quot;300&quot; data-filename=&quot;fig3.jpg&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 3&amp;gt; i 번째 물건이 종류 1인 물건과 매칭되는 경우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$k+1$ 번째 물건부터 $i$ 번째 물건까지 종류 $0$인 물건 개수와 종류 $1$인 물건 개수가 같은 가장 큰 $k$를 찾는다. 이때, $k+1$ 번째 물건부터 $i$ 번째 물건 사이에 있는 물건들은 서로 다른 종류끼리 매칭되는 것이 최적이며, 다음과 같이 매칭 됨을 알 수 있다. 즉, 최소 비용은 D[k]+(종류 $0$인 물건들의 위치 합에 2를 곱한 것)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 코딩할 때 3-2)와 2)는 한번에 고려해줄 수 있다. 이와 같이 DP를 하게 되면 DP 시간복잡도는 $O(N)$이며, 처음에 물건들을 위치를 기준으로 정렬해야 하므로, 전체 시간복잡도는 $O(N \lg N)$이 된다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &lt;bits/stdc++.h&gt;
using namespace std;

using lld = long long;
constexpr lld inf = 1e18;

lld solve(vector&lt;pair&lt;int, int&gt;&gt;&amp; X, int C){
	if (X.empty()) return 0;
	auto put_min = [](auto&amp; a, auto b){
		a = min(a, b);
	};
	sort(begin(X), end(X));
	int N = size(X);
	lld S[N+1][2];
	S[0][0] = S[0][1] = 0;
	for (int i=1;i&lt;=N;i++){
		S[i][0] = S[i-1][0];
		S[i][1] = S[i-1][1];
		S[i][X[i-1].second] += X[i-1].first;
	}
	lld D[N+1]; int last[2*N+1]; memset(last, -1, sizeof(last));
	int acc = 0;
	D[0] = 0; last[N] = 0;
	for (int i=1;i&lt;=N;i++){
		auto [x, s] = X[i-1];
		if (s) acc++;
		else acc--;
		D[i] = D[i-1]+2*x;
		if (i &gt;= 2 &amp;&amp; s == X[i-2].second)
			put_min(D[i], D[i-2]+2*x+C);
		if (int k = last[acc+N]; k != -1)
			put_min(D[i], D[k]+2*(S[i][s]-S[k][s]));
		last[acc+N] = i;
	}
	return D[N];
}

void solve(){
	int N, C;
	scanf(&quot;%d%d&quot;, &amp;N, &amp;C);
	vector&lt;pair&lt;int, int&gt;&gt; A, B;
	for (int i=0;i&lt;N;i++){
		int x, s; scanf(&quot;%d%d&quot;, &amp;x, &amp;s);
		if (x &gt; 0) A.emplace_back(x, s);
		else B.emplace_back(-x, s);
	}
	lld ans = solve(A, C)+solve(B, C);
	printf(&quot;%lld\n&quot;, ans);
}

int main(){
	int T, ts = 0;
	for (scanf(&quot;%d&quot;, &amp;T);T--;){
		printf(&quot;Case #%d: &quot;, ++ts);
		solve();
	}
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;후기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screen Shot 2022-05-16 at 12.40.25 AM.png&quot; data-origin-width=&quot;4850&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjHewW/btrB5ASKqlY/kcCdkmmnfmQalciZg8RmW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjHewW/btrB5ASKqlY/kcCdkmmnfmQalciZg8RmW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjHewW/btrB5ASKqlY/kcCdkmmnfmQalciZg8RmW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjHewW%2FbtrB5ASKqlY%2FkcCdkmmnfmQalciZg8RmW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4850&quot; height=&quot;532&quot; data-filename=&quot;Screen Shot 2022-05-16 at 12.40.25 AM.png&quot; data-origin-width=&quot;4850&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 A가 빠른 시간 안에 풀기 어려운 문제였다. 어떻게든 간단하게 해결하는 방법을 찾으려고 노력하다가 결국 30분에 해결했다. 이때부터 뭔가 말릴 것 같은 느낌이 많이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 B를 읽었는데, 내가 좋아하지 않는 유형의 문제였고, 딱 봤을 때 풀이가 직관적으로 떠오르는 문제는 아니었다. $O(R^2)$ 코드를 짜고 정답에서 규칙이 있지 않을까 들여다봤고, 별다른 규칙이 떠오르지 않아서 B small만 풀고 C로 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C도 풀이가 바로 떠오르지 않아서 small이라도 맞기 위해 brute force를 짰는데, TLE가 나왔다. 고민을 하던 중 완전 매칭 필요조건까지 찾았고, 완전매칭이완전 매칭이 존재하는 경우만 brute force를 돌렸더니 small 통과했다. 이때, 완전 매칭이 존재하면 답이 항상 존재한다는 사실을 채점 결과를 통해 알게 되어서 풀이에 거의 접근을 다 했다. 그러나 정말 바보 같이 사이클의 크기가 2인 경우에 대해서만 재매칭을 시켜줬다. 반신반의로 제출했는데 여전히 small이 통과되어서 &quot;혹시 이게 되나?&quot;라는 생각으로 D로 넘어갔다. 시간은 30분이 남았다. (&lt;span&gt;물론, 싸이클의 크기가 2보다 클 수 있으므로 틀린 풀이다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D small 풀이는 어렵지 않은 DP 연습 문제 수준이었고, 30분 안에 충분히 풀 수 있었는데, 맨 처음에 접근을 잘못했고, 올바른 풀이가 떠올랐지만 시간이 10분 정도 남았다. 10분 안에 코딩하면 됐는데, 그냥 자포자기 상태였던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대회를 진행하면서 시간이 부족한 경우는 정말 오랜만이었다. 이전 라운드에서는 주어진 시간 안에 풀 수 있는 문제, 풀어야 하는 문제는 여유롭게 풀었던 것 같다. A를 푸는데 내가 계획했던 것보다 오래 걸렸던 것부터 말리기 시작하더니, B가 좋아하지 않는 유형의 문제라 자포자기 상태가 되어버린 것 같다. C는 풀이에 접근할 수 있는 충분한 단서가 주어졌음에도 불구하고, 이성적이었다면 접근할 수 있는 풀이를 접근하지 못했고, D small도 주어진 짧은 시간 안에 빠르게 코딩할 수 있었을 텐데, 손이 안 갔다. 대회 다음 날 다시 풀어보는데 아쉬움이 많이 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 스스로 생각했을 때 다음과 같은 결과를 받았으면 좋았을 것 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A: 15분 정도에 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B: 빠르게 small 해결하고 C로 넘어가거나, 운이 좋아서 특징들을 발견하고 large까지 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C: 시간이 오래 걸리더라도 large까지는 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D: 빠르게 small 해결하고 large 손절, B large에 시간 투자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 64점 혹은 80점으로 대회를 마치고 아쉬움이 크지 않았을 것 같다.&lt;/p&gt;</description>
      <category>해법</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/142</guid>
      <comments>https://mwchun.tistory.com/142#entry142comment</comments>
      <pubDate>Mon, 16 May 2022 00:41:03 +0900</pubDate>
    </item>
    <item>
      <title>Hu-Tucker Algorithm</title>
      <link>https://mwchun.tistory.com/141</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;유용한 링크: &lt;a href=&quot;https://math.mit.edu/~shor/PAM/hu-tucker_algorithm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MIT 강의 자료&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 문자열이 있고, 각 알파벳에 바이너리를 할당한다. 할당된 바이너리는 어떤 것이 다른 것의 prefix가 되면 안 된다. 문자열을 알파벳에 할당된 바이너리로 표현할 때 바이너리의 크기를 최소화하는 문제가 있다. 이는 매우 일반적인 압축 알고리즘을 필요로 하는 상황이다. &lt;a href=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Huffman Coding&lt;/a&gt;은 이 문제에 대한 최적해를 $O(N \lg N)$ 시간에 구한다. 각색은 다르지만, Huffman Coding과 같은 상황인 문제는 &lt;a href=&quot;https://www.acmicpc.net/problem/13975&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 13975번 파일 합치기 3&lt;/a&gt;이 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1651513524777&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Huffman coding - Wikipedia&quot; data-og-description=&quot;From Wikipedia, the free encyclopedia Jump to navigation Jump to search Technique to compress data Huffman tree generated from the exact frequencies of the text &amp;quot;this is an example of a huffman tree&amp;quot;. The frequencies and codes of each character are below. &quot; data-og-host=&quot;en.wikipedia.org&quot; data-og-source-url=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot; data-og-url=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ejpiea/hyOgpI1dLm/LlXLV4phziKz6oUpmiObR1/img.png?width=1200&amp;amp;height=772&amp;amp;face=0_0_1200_772,https://scrap.kakaocdn.net/dn/dLG6GG/hyOgvJewUX/UvlGwWABYecC2uIqQSQaZ0/img.png?width=800&amp;amp;height=515&amp;amp;face=0_0_800_515,https://scrap.kakaocdn.net/dn/cmeRgR/hyOe7DcShZ/1j3KEJF2m9Xl4WMYFmMDG0/img.png?width=640&amp;amp;height=412&amp;amp;face=0_0_640_412&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ejpiea/hyOgpI1dLm/LlXLV4phziKz6oUpmiObR1/img.png?width=1200&amp;amp;height=772&amp;amp;face=0_0_1200_772,https://scrap.kakaocdn.net/dn/dLG6GG/hyOgvJewUX/UvlGwWABYecC2uIqQSQaZ0/img.png?width=800&amp;amp;height=515&amp;amp;face=0_0_800_515,https://scrap.kakaocdn.net/dn/cmeRgR/hyOe7DcShZ/1j3KEJF2m9Xl4WMYFmMDG0/img.png?width=640&amp;amp;height=412&amp;amp;face=0_0_640_412');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Huffman coding - Wikipedia&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;From Wikipedia, the free encyclopedia Jump to navigation Jump to search Technique to compress data Huffman tree generated from the exact frequencies of the text &quot;this is an example of a huffman tree&quot;. The frequencies and codes of each character are below.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;en.wikipedia.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hu-Tucker algorithm은 위와 같은 상황이지만, 알파벳의 순서가 중요한 경우에 필요한 알고리즘이다. 즉, 사전순으로사전 순으로 앞에 오는 알파벳은 할당된 바이너리도 사전 순으로 앞에 오도록 하고 싶은 상황에 필요하다. &lt;a href=&quot;https://blog.myungwoo.kr/140&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Skew heap&lt;/a&gt;을 이용하여 구현하면, $O(N \lg N)$ 시간안에 구현할 수 있다. 마찬가지로 각색은 다르지만, Hu-Tucker algorithm이 필요한 문제로 &lt;a href=&quot;https://www.acmicpc.net/problem/19089&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 19089번 파일 합치기 4&lt;/a&gt;가 있다. 상황과 비슷한 각색의 문제로 &lt;a href=&quot;https://atcoder.jp/contests/atc002/tasks/atc002_c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AtCoder Typical Contest 002 C 최적 이분 탐색 트리 문제&lt;/a&gt;가 있다. 두 문제가 비슷한 입력 형식을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hu-Tucker algorithm에서 Huffman Coding과 마찬가지로 두 빈도를 합쳐나가는 과정을 반복하지만, 두 수를 합칠 수 있는 조건이 다르다. Huffman Coding에서는 빈도 수가 가장 적은 두 값을 하나로 합쳤었다. Hu-Tucker algorithm에서는 두 수 사이에 &lt;b&gt;기존 수&lt;/b&gt;가 하나도 없으면 하나로 합칠 수 있다고 한다. 합칠 수 있는 두 수가 있으면 두 수의 합이 가장 작은 것들부터 합친다. 이해를 돕기 위해 예시 상황을 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 각 알파벳이 등장하는 횟수(빈도)가 있다고 하자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[&lt;b&gt;1&lt;/b&gt;, &lt;b&gt;2&lt;/b&gt;, &lt;b&gt;23&lt;/b&gt;, &lt;b&gt;4&lt;/b&gt;, &lt;b&gt;3&lt;/b&gt;, &lt;b&gt;3&lt;/b&gt;, &lt;b&gt;5&lt;/b&gt;, &lt;b&gt;19&lt;/b&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 인접한 두 수만 합칠 수 있다. 인접한 두 수 중 합이 제일 작은 쌍은 (1, 2)다. 둘을 합쳐 3을 넣자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[3, &lt;b&gt;23&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;4&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;3&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;3&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;5&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;19&lt;/b&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 3은 볼드 처리가 안되어있는데, 이는 &lt;b&gt;기존 수&lt;/b&gt;가 아닌 새로 추가된 수임을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 합이 제일 작은 합칠 수 있는 두 수는 (3, 3)이다. 둘을 합쳐 6을 넣자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[3, &lt;b&gt;23&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;4&lt;/b&gt;&lt;span&gt;,&lt;span&gt; 6&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;5&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;19&lt;/b&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 4와 5는 서로 합칠 수 있는데, 4와 5 사이에는 &lt;b&gt;기존 수&lt;/b&gt;가 하나도 없기 때문이다. 합이 제일 작은 합칠 수 있는 두 수는 (4, 5)다. 둘을 합쳐 9를 넣자. 9가 들어갈 수 있는 위치는 기존 4와 5 사이 어디든 상관없다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[3, &lt;b&gt;23&lt;/b&gt;&lt;span&gt;,&lt;span&gt; 9, 6&lt;/span&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;19&lt;/b&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 합이 제일 작은 합칠 수 있는 두 수는 (9, 6)이다. 둘을 합쳐 15를 넣자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[3, &lt;b&gt;23&lt;/b&gt;, 15, &lt;b&gt;19&lt;/b&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 합이 제일 작은 합칠 수 있는 두 수는 (3, 23)이다. 둘을 합쳐 26을 넣자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[26, 15, &lt;b&gt;19&lt;/b&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 합이 제일 작은 합칠 수 있는 두 수는 (15, 19)이다. 둘을 합쳐 34를 넣자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[26, 34]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 합칠 수 있는 두 수는 (26, 34)이다. 둘을 합쳐 60을 넣자.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[60]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수가 하나만 남았으므로 알고리즘은 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 합친 과정을 그림으로 나타내면 다음과 같다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B7kgF/btrA33n4U70/BP1ET2UgKPAjFF4oXoiT81/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B7kgF/btrA33n4U70/BP1ET2UgKPAjFF4oXoiT81/img.gif&quot; data-alt=&quot;&amp;amp;lt;그림 1&amp;amp;gt; Hu-Tucker algorithm의 초기 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B7kgF/btrA33n4U70/BP1ET2UgKPAjFF4oXoiT81/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/B7kgF/btrA33n4U70/BP1ET2UgKPAjFF4oXoiT81/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;328&quot; height=&quot;278&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 1&amp;gt; Hu-Tucker algorithm의 초기 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 초기 결과에서 해결해야 할 문제가 하나 있다. 바로, 9와 6이 만들어지는 선분들이 서로 교차하고 있기 때문에 이 트리에서 올바른 인코딩을 구할 수 없다. 위 이진트리에서 각 리프 정점에 루트와의 거리(즉, 노드의 깊이이자 할당된 바이너리 길이)를 적어보면 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;[1(3), 2(3), 23(2), 4(4), 3(4), 3(4), 5(4), 19(2)]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이진트리를 올바르게 만들어주기 위해 다음과 같은 과정을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 먼저 제일 깊은 깊이가 4인 정점 4개를 순서대로 이어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;148&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxKQVQ/btrA6yUEsJe/2axbGACLpcgKHyqVGlpmRk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxKQVQ/btrA6yUEsJe/2axbGACLpcgKHyqVGlpmRk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxKQVQ/btrA6yUEsJe/2axbGACLpcgKHyqVGlpmRk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bxKQVQ/btrA6yUEsJe/2axbGACLpcgKHyqVGlpmRk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;148&quot; height=&quot;91&quot; data-origin-width=&quot;148&quot; data-origin-height=&quot;91&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊이가 4인 정점을 2쌍 이어주었기 때문에 깊이가 3인 정점이 2개 더 생겨서 4개 있는 것과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WrSJs/btrA41XfGBH/ep0BxB0lJkPV5tXRhgbiqK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WrSJs/btrA41XfGBH/ep0BxB0lJkPV5tXRhgbiqK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WrSJs/btrA41XfGBH/ep0BxB0lJkPV5tXRhgbiqK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/WrSJs/btrA41XfGBH/ep0BxB0lJkPV5tXRhgbiqK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;93&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 깊이가 3인 정점 4개를 순서대로 이어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ch2tOD/btrA3tmYrZZ/pbCxhE6SXDoJNvtsp8R1M0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ch2tOD/btrA3tmYrZZ/pbCxhE6SXDoJNvtsp8R1M0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ch2tOD/btrA3tmYrZZ/pbCxhE6SXDoJNvtsp8R1M0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ch2tOD/btrA3tmYrZZ/pbCxhE6SXDoJNvtsp8R1M0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;343&quot; height=&quot;148&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊이가 2인 정점 4개를 순서대로 이어주고, 깊이가 1인 정점에 대해서도 처리하면 다음과 같은 결과가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUfJx8/btrA3hzvtJ4/jL4YHpiL99YePU67UkRWc1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUfJx8/btrA3hzvtJ4/jL4YHpiL99YePU67UkRWc1/img.gif&quot; data-alt=&quot;&amp;amp;lt;그림 2&amp;amp;gt; Hu-Tucker algorithm의 최종 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUfJx8/btrA3hzvtJ4/jL4YHpiL99YePU67UkRWc1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bUfJx8/btrA3hzvtJ4/jL4YHpiL99YePU67UkRWc1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;353&quot; height=&quot;251&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 2&amp;gt; Hu-Tucker algorithm의 최종 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 알고리즘이 올바르게 동작하기 위해서 다음 명제들을 증명해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Hu-Tucker algorithm의 초기 결과에서 위와 같은 방법으로 올바른 트리를 항상 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Hu-Tucker algorithm의 초기 결과의 비용(각 알파벳의 빈도 수와 할당된 바이너리 길이를 곱한 값들의 합)이 최적해다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 명제에 대한 증명은 &lt;a href=&quot;https://math.mit.edu/~shor/PAM/hu-tucker_algorithm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MIT 강의 자료&lt;/a&gt;에 나와있고, 2번 명제에 대한 증명은 나와있지 않다. 아무래도 2번 명제에 대한 증명이 매우 까다로운 듯하다. 강의 자료에 따르면 초기 논문들에서 나온 증명은 틀렸다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 그러면 마지막으로 남은 것은 이것을 구현하는 방법에 대한 것이다. 이 알고리즘은 증명도 까다롭고, 구현도 간단하지 않다. 다음 그림은 Hu-Tucker algorithm의 중간과정을 그림으로 표현한 것이다. 기존 수는 검은 동그라미, 기존 수가 아닌 것은 파란 동그라미로 그려졌다. 초록색 덩어리들을 하나로 묶어서 &lt;a href=&quot;https://blog.myungwoo.kr/140&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Skew heap&lt;/a&gt;(두 힙을 하나로 합치는데 $O(\lg n)$ 시간이 걸리는 힙)으로 구현이 비교적 간단하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpIpDm/btrAYtHdvNz/dT6saAG16XDE1qHksWVCN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpIpDm/btrAYtHdvNz/dT6saAG16XDE1qHksWVCN1/img.png&quot; data-alt=&quot;&amp;amp;lt;그림 3&amp;amp;gt; Hu-Tucker algorithm 구현 중 Skew heap 관리 방법&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpIpDm/btrAYtHdvNz/dT6saAG16XDE1qHksWVCN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpIpDm%2FbtrAYtHdvNz%2FdT6saAG16XDE1qHksWVCN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1590&quot; height=&quot;142&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 3&amp;gt; Hu-Tucker algorithm 구현 중 Skew heap 관리 방법&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초록 직사각형이 하나의 Skew heap을 나타내고, heap 안에는 초록색 직사각형 안에 있는 파란 동그라미에 적힌 빈도수가 들어있다. 자세한 구현은 아래 첨부된 코드를 참고하자. 첨부된 코드는 위에서 소개한 &lt;a href=&quot;https://www.acmicpc.net/problem/19089&quot;&gt;BOJ 19089번 파일 합치기 4&lt;/a&gt; 문제를 푸는 코드다. 시간복잡도는 $O(N \lg N)$이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

template&amp;lt;typename T&amp;gt;
struct SkewHeap{
	using value_t = T;
	struct Node;
	using node_t = Node;
	using nodeptr_t = unique_ptr&amp;lt;node_t&amp;gt;;
	struct Node{
		Node() = default;
		Node(const T&amp;amp; value): value(value){}
		value_t value;
		nodeptr_t left = nullptr, right = nullptr;
		static nodeptr_t merge(nodeptr_t t1, nodeptr_t t2){
			if (t1 == nullptr) return t2;
			if (t2 == nullptr) return t1;
			if (t2-&amp;gt;value &amp;lt; t1-&amp;gt;value) return merge(move(t2), move(t1));
			nodeptr_t tmp = move(t1-&amp;gt;right);
			t1-&amp;gt;right = move(t1-&amp;gt;left);
			t1-&amp;gt;left = merge(move(t2), move(tmp));
			return t1;
		}
	};
	nodeptr_t m_root{};
	size_t m_size{};

	bool empty()const{ return m_size == 0; }
	size_t size()const{ return m_size; }
	value_t top()const{
		return m_root-&amp;gt;value;
	}
	value_t second_top()const{
		if (m_root-&amp;gt;left == nullptr) return m_root-&amp;gt;right-&amp;gt;value;
		if (m_root-&amp;gt;right == nullptr) return m_root-&amp;gt;left-&amp;gt;value;
		return min(m_root-&amp;gt;left-&amp;gt;value, m_root-&amp;gt;right-&amp;gt;value);
	}
	value_t pop(){
		--m_size;
		auto value = m_root-&amp;gt;value;
		m_root = node_t::merge(move(m_root-&amp;gt;left), move(m_root-&amp;gt;right));
		return value;
	}
	void push(const value_t&amp;amp; value){
		++m_size;
		m_root = node_t::merge(move(m_root), nodeptr_t{new node_t(value)});
	}
	void merge(SkewHeap&amp;lt;value_t&amp;gt;&amp;amp;&amp;amp; other){
		m_size += other.m_size;
		m_root = node_t::merge(move(m_root), move(other.m_root));
		other.m_size = 0;
		other.m_root = nullptr;
	}
};

template&amp;lt;typename T&amp;gt;
struct HuTucker{
	using value_t = T;
	static constexpr value_t inf = numeric_limits&amp;lt;value_t&amp;gt;::max();
	HuTucker(const vector&amp;lt;T&amp;gt;&amp;amp; arr){
		N = size(arr);
		W.resize(N+2); C.resize(N+2); L.resize(N+2); R.resize(N+2);
		for (int i=0;i&amp;lt;N;i++) W[i+1] = arr[i];
		for (int i=0;i&amp;lt;=N;i++) R[i] = i+1, L[i+1] = i;
		W[0] = W[N+1] = inf;
		heaps.resize(N+1);
		for (int i=1;i&amp;lt;N;i++){
			C[i] = W[i]+W[i+1];
			main.emplace(-C[i], i);
		}
	}
	value_t get_min(int i){
		value_t ret = inf;
		if (W[i] != inf &amp;amp;&amp;amp; W[R[i]] != inf) ret = min(ret, W[i]+W[R[i]]);
		if (W[i] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1) ret = min(ret, W[i]+heaps[i].top());
		if (W[R[i]] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1) ret = min(ret, W[R[i]]+heaps[i].top());
		if (heaps[i].size() &amp;gt;= 2) ret = min(ret, heaps[i].top()+heaps[i].second_top());
		return ret;
	}
	value_t solve(){
		value_t ans{};
		for (int elements=N;elements&amp;gt;1;){
			auto [c, i] = main.top(); main.pop(); c = -c;
			if (C[i] != c) continue;
			if (heaps[i].size() &amp;gt;= 2 &amp;amp;&amp;amp; heaps[i].top()+heaps[i].second_top() == c){
				heaps[i].pop(); heaps[i].pop();
			}
			else if (W[i] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1 &amp;amp;&amp;amp; W[i]+heaps[i].top() == c){
				C[i] = W[i] = inf;
				R[L[i]] = R[i];
				L[R[i]] = L[i];
				heaps[i].pop();
				heaps[L[i]].merge(move(heaps[i]));
				i = L[i];
			}
			else if (W[R[i]] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1 &amp;amp;&amp;amp; W[R[i]]+heaps[i].top() == c){
				C[R[i]] = W[R[i]] = inf;
				heaps[i].pop();
				L[R[R[i]]] = i;
				heaps[i].merge(move(heaps[R[i]]));
				R[i] = R[R[i]];
			}
			else if (W[i] != inf &amp;amp;&amp;amp; W[R[i]] != inf &amp;amp;&amp;amp; W[i]+W[R[i]] == c){
				C[i] = C[R[i]] = W[i] = W[R[i]] = inf;
				R[L[i]] = R[R[i]];
				L[R[R[i]]] = L[i];
				heaps[i].merge(move(heaps[R[i]]));
				heaps[L[i]].merge(move(heaps[i]));
				i = L[i];
			}
			else assert(0);
			heaps[i].push(c); C[i] = get_min(i);
			main.emplace(-C[i], i);
			--elements; ans += c;
		}
		return ans;
	}
	int N;
	vector&amp;lt;value_t&amp;gt; W, C;
	vector&amp;lt;int&amp;gt; L, R;
	vector&amp;lt;SkewHeap&amp;lt;value_t&amp;gt;&amp;gt; heaps;
	priority_queue&amp;lt;pair&amp;lt;value_t, int&amp;gt;&amp;gt; main;
};

void solve(){
	int n; scanf(&quot;%d&quot;, &amp;amp;n);
	vector&amp;lt;lld&amp;gt; arr(n);
	for (lld&amp;amp; v: arr) scanf(&quot;%lld&quot;, &amp;amp;v);
	printf(&quot;%lld\n&quot;, HuTucker(arr).solve());
}

int main(){
	int T;
	for (scanf(&quot;%d&quot;, &amp;amp;T);T--;) solve();
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담) Skew heap을 쓰지 않고, 일반적인 heap을 이용하여 구현도 가능하다. 임의의 두 heap $A$와 $B$를 합치는 과정을 알아보자. 일반성을 잃지 않고 $|A| \geq |B|$라고 하자. 이때, $B$의 원소들을 하나씩 pop 하면서, $A$에 push 해주면 전체 시간복잡도가 $O(N \lg^2 N)$으로 늘어나지만 실제 실행 시간이 Skew heap을 사용했을 때보다 조금 더 빨랐다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;코드 보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

using lld = long long;

template&amp;lt;typename T&amp;gt;
struct HuTucker{
	using value_t = T;
	using heap_t = priority_queue&amp;lt;value_t, vector&amp;lt;value_t&amp;gt;, greater&amp;lt;value_t&amp;gt;&amp;gt;;
	static value_t second_top(heap_t&amp;amp; h){
		auto a = h.top(); h.pop();
		auto b = h.top();
		h.push(a);
		return b;
	}
	static void merge(heap_t&amp;amp; a, heap_t&amp;amp; b){
		if (size(a) &amp;lt; size(b)) a.swap(b);
		while (!b.empty()) a.push(b.top()), b.pop();
	}
	static constexpr value_t inf = numeric_limits&amp;lt;value_t&amp;gt;::max();
	HuTucker(const vector&amp;lt;T&amp;gt;&amp;amp; arr){
		N = size(arr);
		W.resize(N+2); C.resize(N+2); L.resize(N+2); R.resize(N+2);
		for (int i=0;i&amp;lt;N;i++) W[i+1] = arr[i];
		for (int i=0;i&amp;lt;=N;i++) R[i] = i+1, L[i+1] = i;
		W[0] = W[N+1] = inf;
		heaps.resize(N+1);
		for (int i=1;i&amp;lt;N;i++){
			C[i] = W[i]+W[i+1];
			main.emplace(-C[i], i);
		}
	}
	value_t get_min(int i){
		value_t ret = inf;
		if (W[i] != inf &amp;amp;&amp;amp; W[R[i]] != inf) ret = min(ret, W[i]+W[R[i]]);
		if (W[i] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1) ret = min(ret, W[i]+heaps[i].top());
		if (W[R[i]] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1) ret = min(ret, W[R[i]]+heaps[i].top());
		if (heaps[i].size() &amp;gt;= 2) ret = min(ret, heaps[i].top()+second_top(heaps[i]));
		return ret;
	}
	value_t solve(){
		value_t ans{};
		for (int elements=N;elements&amp;gt;1;){
			auto [c, i] = main.top(); main.pop(); c = -c;
			if (C[i] != c) continue;
			if (heaps[i].size() &amp;gt;= 2 &amp;amp;&amp;amp; heaps[i].top()+second_top(heaps[i]) == c){
				heaps[i].pop(); heaps[i].pop();
			}
			else if (W[i] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1 &amp;amp;&amp;amp; W[i]+heaps[i].top() == c){
				C[i] = W[i] = inf;
				R[L[i]] = R[i];
				L[R[i]] = L[i];
				heaps[i].pop();
				merge(heaps[L[i]], heaps[i]);
				i = L[i];
			}
			else if (W[R[i]] != inf &amp;amp;&amp;amp; heaps[i].size() &amp;gt;= 1 &amp;amp;&amp;amp; W[R[i]]+heaps[i].top() == c){
				C[R[i]] = W[R[i]] = inf;
				heaps[i].pop();
				L[R[R[i]]] = i;
				merge(heaps[i], heaps[R[i]]);
				R[i] = R[R[i]];
			}
			else if (W[i] != inf &amp;amp;&amp;amp; W[R[i]] != inf &amp;amp;&amp;amp; W[i]+W[R[i]] == c){
				C[i] = C[R[i]] = W[i] = W[R[i]] = inf;
				R[L[i]] = R[R[i]];
				L[R[R[i]]] = L[i];
				merge(heaps[i], heaps[R[i]]);
				merge(heaps[L[i]], heaps[i]);
				i = L[i];
			}
			else assert(0);
			heaps[i].push(c); C[i] = get_min(i);
			main.emplace(-C[i], i);
			--elements; ans += c;
		}
		return ans;
	}
	int N;
	vector&amp;lt;value_t&amp;gt; W, C;
	vector&amp;lt;int&amp;gt; L, R;
	vector&amp;lt;heap_t&amp;gt; heaps;
	priority_queue&amp;lt;pair&amp;lt;value_t, int&amp;gt;&amp;gt; main;
};

void solve(){
	int n; scanf(&quot;%d&quot;, &amp;amp;n);
	vector&amp;lt;lld&amp;gt; arr(n);
	for (lld&amp;amp; v: arr) scanf(&quot;%lld&quot;, &amp;amp;v);
	printf(&quot;%lld\n&quot;, HuTucker(arr).solve());
}

int main(){
	int T;
	for (scanf(&quot;%d&quot;, &amp;amp;T);T--;) solve();
}&lt;/textarea&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>공부</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/141</guid>
      <comments>https://mwchun.tistory.com/141#entry141comment</comments>
      <pubDate>Tue, 3 May 2022 03:23:43 +0900</pubDate>
    </item>
    <item>
      <title>Skew Heap</title>
      <link>https://mwchun.tistory.com/140</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;유용한 링크: &lt;a href=&quot;https://en.wikipedia.org/wiki/Skew_heap#:~:text=A%20skew%20heap%20(or%20self,more%20quickly%20than%20binary%20heaps.&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키백과&lt;/a&gt;, &lt;a href=&quot;https://www.cs.usfca.edu/~galles/visualization/SkewHeap.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Skew heap visualization&lt;/a&gt;, &lt;a href=&quot;http://ccf.ee.ntu.edu.tw/~yen/courses/ds17/chapter-6d.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NTU 강의자료&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skew heap(혹은 self-adjusting heap)은 이진트리로 구현된 힙 자료구조다. 우리가 배운 기본적인 힙은 완전이진트리이므로 배열과 인덱스를 이용하여 편하게 구현이 가능하다. 그러나, Skew heap은 완전이진트리가 아닌 그냥 이진트리이므로 배열을 이용한 구현을 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 일반 힙이 아닌 Skew heap이 필요한 순간은 언제일까? 바로, 서로 다른 두 힙을 하나로 빠르게 합치고 싶은 순간에 쓸 수 있다. 서로 다른 두 힙을 하나로 합치는 것을 merge 연산이라고 한다. Skew heap은 merge 연산 중간, 왼쪽 자식과 오른쪽 자식을 무조건적으로 바꿔주어 균형을 유지한다. 이 merge 연산은 Skew heap에 원소를 추가(push)하거나, 루트에 있는 값을 제거(pop)할 때도 사용된다. 힙이 균형잡혀 있기 때문에 merge 연산은 amortised $O(\lg n)$ 시간복잡도를 가진다. 좀 더 정확히 하자면, 황금비 $\varphi = \frac{1+\sqrt{5}}{2}$가 있을 때, $O(\log_{\varphi} n)$이 된다. 이는 약 $1.44 \lg n$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skew heap은 다음과 같이 정의된다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 원소가 하나인 힙은 Skew heap이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 두 Skew heap을 merge한 결과는 Skew heap이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 두 Skew heap을 하나로 합치는 merge 연산이 어떻게 동작하는지 알아보자. Merge 연산이 하는 일은 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 합치려고 하는 두 Skew heap 중 루트가 작은 것을 $t_1$, 나머지를 $t_2$라 하자. 그리고 만들려고 하는 새로운 힙을 $r$이라 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. $r$의 루트는 $t_1$의 루트가 되며, $r$의 오른쪽 서브트리는 $t_1$의 왼쪽 서브트리가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 재귀적으로 $t_2$와 $t_1$의 오른쪽 서브트리를 합쳐 하나의 힙으로 만든 뒤, 그 힙을 $r$의 왼쪽 서브트리로 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 merge 연산의 예시이다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/psJeO/btrA5a712ye/HBZVN49j4tbqiF6yVILuCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/psJeO/btrA5a712ye/HBZVN49j4tbqiF6yVILuCk/img.png&quot; data-alt=&quot;&amp;amp;lt;그림 1&amp;amp;gt; 합치려고 하는 두 Skew Heap&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/psJeO/btrA5a712ye/HBZVN49j4tbqiF6yVILuCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpsJeO%2FbtrA5a712ye%2FHBZVN49j4tbqiF6yVILuCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;116&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 1&amp;gt; 합치려고 하는 두 Skew Heap&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3SoT9/btrA5rnNvCI/r8vTRzPK2zF6OjTCWp1Ca0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3SoT9/btrA5rnNvCI/r8vTRzPK2zF6OjTCWp1Ca0/img.png&quot; data-alt=&quot;&amp;amp;lt;그림 2&amp;amp;gt; 합쳐진 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3SoT9/btrA5rnNvCI/r8vTRzPK2zF6OjTCWp1Ca0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3SoT9%2FbtrA5rnNvCI%2Fr8vTRzPK2zF6OjTCWp1Ca0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;255&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;그림 2&amp;gt; 합쳐진 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, push와 pop은 어떻게 구현할 수 있을까? 원소를 삽입하는 것은 크기가 1인 Skew heap과의 merge로 구현할 수 있고, 루트를 삭제하는 것은 루트의 왼쪽 서브트리에 해당하는 Skew heap과 오른쪽 서브트리에 해당하는 Skew heap을 합치는 것으로 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skew heap을 C++으로 구현하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;textarea class=&quot;brush:cpp;&quot; name=&quot;code&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

template&amp;lt;typename T&amp;gt;
struct SkewHeap{
	using value_t = T;
	struct Node;
	using node_t = Node;
	using nodeptr_t = unique_ptr&amp;lt;node_t&amp;gt;;
	struct Node{
		Node() = default;
		Node(const T&amp;amp; value): value(value){}
		value_t value;
		nodeptr_t left = nullptr, right = nullptr;
		static nodeptr_t merge(nodeptr_t t1, nodeptr_t t2){
			if (t1 == nullptr) return t2;
			if (t2 == nullptr) return t1;
			if (t2-&amp;gt;value &amp;lt; t1-&amp;gt;value) return merge(move(t2), move(t1));
			nodeptr_t tmp = move(t1-&amp;gt;right);
			t1-&amp;gt;right = move(t1-&amp;gt;left);
			t1-&amp;gt;left = merge(move(t2), move(tmp));
			return t1;
		}
	};
	nodeptr_t m_root{};
	size_t m_size{};

	bool empty()const{ return m_size == 0; }
	size_t size()const{ return m_size; }
	value_t top()const{
		return m_root-&amp;gt;value;
	}
	value_t second_top()const{
		if (m_root-&amp;gt;left == nullptr) return m_root-&amp;gt;right-&amp;gt;value;
		if (m_root-&amp;gt;right == nullptr) return m_root-&amp;gt;left-&amp;gt;value;
		return min(m_root-&amp;gt;left-&amp;gt;value, m_root-&amp;gt;right-&amp;gt;value);
	}
	value_t pop(){
		--m_size;
		auto value = m_root-&amp;gt;value;
		m_root = node_t::merge(move(m_root-&amp;gt;left), move(m_root-&amp;gt;right));
		return value;
	}
	void push(const value_t&amp;amp; value){
		++m_size;
		m_root = node_t::merge(move(m_root), nodeptr_t{new node_t(value)});
	}
	void merge(SkewHeap&amp;lt;value_t&amp;gt;&amp;amp;&amp;amp; other){
		m_size += other.m_size;
		m_root = node_t::merge(move(m_root), move(other.m_root));
		other.m_size = 0;
		other.m_root = nullptr;
	}
};&lt;/textarea&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Skew heap은 &lt;a href=&quot;https://mwchun.tistory.com/141&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hu-Tucker algorithm&lt;/a&gt;을 구현할 때 활용할 수 있다.&lt;/p&gt;</description>
      <category>공부</category>
      <author>전명우</author>
      <guid isPermaLink="true">https://mwchun.tistory.com/140</guid>
      <comments>https://mwchun.tistory.com/140#entry140comment</comments>
      <pubDate>Tue, 3 May 2022 02:00:49 +0900</pubDate>
    </item>
  </channel>
</rss>