読者です 読者をやめる 読者になる 読者になる

はまやんはまやんはまやん

hamayanhamayan's blog

PolandBall and Gifts [Codeforces 8VC Venture Cup 2017 - Elimination Round : F]

問題

http://codeforces.com/contest/755/problem/F

N人がプレゼントを持っていて、iさんがP[i]さんに渡す。
このうち、K人がプレゼントを忘れてくる。
忘れてくる任意の組合せのうち、以下のルールによりプレゼントが貰えない人数の最大・最小を答えよ。

以下の2つのいずれかを満たすと、プレゼントがもらえない

  • プレゼントを忘れた
  • プレゼントをくれるはずの人がプレゼントを忘れた

2 <= N <= 10^6
0 <= K <= N
P[i]は1~Nの順列

参考

これは色んな所を参考にして、やっと理解した。
後続者のためにどういう思考の流れか、メモしておく。
http://kenkoooo.hatenablog.com/entry/2017/01/16/053254
http://codeforces.com/contest/755/submission/23854346
http://codeforces.com/blog/entry/49793

解法

1. プレゼント先をグラフで考えると、鎖状のグラフが幾つかある状態になる
2. なので、cnt[i]を長さiの鎖状のグラフの個数と置き、これを何らかの方法で求めておく -> pre()

3. 次に公式Editorialにもあるが、最大は貪欲に考えて行けばよい
4. 自分の解法では、1つのプレゼント忘れでなるべく2人もらえないようにして、それが全部終わったら、1つのプレゼント忘れで1人もらえないようにしている -> solve_max()

5. 問題は最小である
6. Twitterや解説では「ナップサック問題」であると言われているが、これはどういうことだろう

7. まず「最小はKかK+1のどちらかである」ということ
8. 最小にするなら1つのプレゼント忘れでなるべく1人もらえないようにしたい
9. 最適に選択していくと1つのプレゼント忘れで2人もらえないことは高々1回しか起きないようにできる
(任意の2箇所で1つのプレゼント忘れで2人もらえない状況があれば、1つのプレゼント忘れで1人もらえない状況と1つのプレゼント忘れで2人もらえない状況に置き換えられる)
10. 全ての状況で1つのプレゼント忘れで1人もらえないのは、「全ての鎖状のグラフに対して、そのグラフの中の全ての人がプレゼントを忘れている、か、全ての人がプレゼントを忘れていない」ときである

11. 7.~10.をまとめると「鎖状のグラフを任意個選んで、人数の総和を取った時にそれがKであれば最小はKであり、そうでなければK+1である」
12. ナップサック

13. 実際に解く場合はナップサックでおなじみのDPを使う
14. dp[i] = 鎖状のグラフを任意個選んで人数の総和をiにできるか

15. 愚直に書くと以下のようになる

int dp[1010101];
int solve_min() {
	dp[0] = 1;
	rep(i, 2, N + 1) {
		rep(j, 1, cnt[i] + 1) {
			rep(k, 0, K) {
				int idx = k + i * j;
				if (idx < 1010101) dp[idx] = 1;
			}
		}
	}

	if (dp[K])
		return K;
	else
		return K + 1;
}

16. これだと余裕でTLE
17. ここでよくあるbitsetによるdpの高速化を行う

bitset<1010101> dp;
int solve_min() {
	dp[0] = 1;
	bitset<1010101> b;
	rep(i, 2, N + 1) {
		b = dp;
		rep(j, 1, cnt[i] + 1) {
			dp |= b << (i * j);
		}
	}

	if (dp[K])
		return K;
	else
		return K + 1;
}

18. これでもTLEした(bitsetの演算のせい?)
19. 更新にO(cnt[i])かかっているが、以下のようにやるとO(log(cnt[i]))にできる

bitset<1010101> dp;
int solve_min() {
	dp[0] = 1;
	bitset<1010101> b;
	rep(i, 2, N + 1) {
		int j = 1;
		while (0 < cnt[i]) {
			j = min(j, cnt[i]);
			dp |= dp << (i * j);
			cnt[i] -= j;
			j *= 2;
		}
	}

	if (dp[K])
		return K;
	else
		return K + 1;
}

20. 理解が難しいかと思うが、bit的には以下の様なことをやっている

例) cnt[2] = 3, dp = 00000001
<j = 1>
dp |= dp << 2
= 00000101
<j = 2>
dp |= dp << 4
= 00010101
<j = min(4,3) = 3>
dp |= dp << 6
= 01010101

21. 説明しづらいが1->2->4->8->16みたいにシフト演算を伝搬させている
22. ここまでやって、やっとAC

解答

#define rrep(i,a,b) for(int i=a;i>=b;i--)
int N, K;
int P[1010101];
//-----------------------------------------------------------------
int cnt[1010101];
int v[1010101];
void pre() {
	rep(i, 0, N) if (!v[i]) {
		int c = 0;
		int j = i;
		while (!v[j]) {
			c++;
			v[j] = 1;
			j = P[j];
		}
		cnt[c]++;
	}
}
//-----------------------------------------------------------------
int solve_max() {
	int kk = K;
	int ret = 0;
	int cc = 0;
	rrep(i, N, 2) {
		int d = i / 2;
		if (kk <= d * cnt[i]) {
			return ret + kk * 2;
		}

		kk -= d * cnt[i];
		if (i % 2) cc += cnt[i];
		ret += d * cnt[i] * 2;
	}

	ret += min(kk, cc);

	return ret;
}
//-----------------------------------------------------------------
bitset<1010101> dp;
int solve_min() {
	dp[0] = 1;
	bitset<1010101> b;
	rep(i, 2, N + 1) {
		int j = 1;
		while (0 < cnt[i]) {
			j = min(j, cnt[i]);
			dp |= dp << (i * j);
			cnt[i] -= j;
			j *= 2;
		}
	}

	if (dp[K])
		return K;
	else
		return K + 1;
}
//-----------------------------------------------------------------
int main() {
	cin >> N >> K;
	rep(i, 0, N) scanf("%d", &P[i]), P[i]--;

	pre();
	int ma = solve_max();
	int mi = solve_min();

	cout << mi << " " << ma << endl;
}