1. 程式人生 > 實用技巧 >【UR #6】懶癌

【UR #6】懶癌

題目無限次看錯……當然如果看對了還是做不出。注意一下只會開一槍,然後全域性結束。

當然官方題解已經講的很清楚了,所以還是特別推薦官方題解。


為了更好地理解,先從完全圖入手(順便拿一下部分分)。

假設有 \(K\) 個人,那麼沒懶狗的會看到 \(K\) 條懶狗,有懶狗的會看到 \(K - 1\) 條。

在第一天,如果沒有槍聲,說明沒有人剛好看到 \(0\) 條,說明大家都至少看到 \(1\) 條。

在第二天,如果有人剛好看到了 \(1\) 條,由於沒有 \(0\) 條,則他剛好是最小值 \(K - 1\),說明他有懶狗;如果沒有槍聲,說明沒有人剛好看到 \(1\) 條。

……

歸納可證明所有有懶狗的人都在第 \(K\)

天槍斃懶狗。


再說一般圖,相比於完全圖,每個人多了一些看不到的狗。

但是,不變的是,每個人都在各自 DP,你預判到了我預判了你的預判,都是在一定的天數積累下得到自己的狗是懶狗。

也就是說,我們要設計一個 dp,得到一個閾值,在這個閾值之後找到懶狗。

那麼:

  • 如果有人要槍斃狗,肯定是在之前沒聽到槍聲。
  • 一個人在若干天之後,如果沒聽到槍聲就會確定自己的狗是懶狗。
  • 對於確定的懶狗集合,其槍斃時間是最早有人確定自己的狗是懶狗的時間。
  • 對於一個人,他會列舉看不見的狗的所有情況,直到完全確定自己的狗是懶狗:
    • 即他會假設懶狗集合,使得自己的狗不在裡面,看得見的狗狀態不變,看不見的狗隨便。
    • 當這個集合槍斃時間的最大值過了,還沒有槍聲,就與自己的狗不是懶狗矛盾,於是就會槍斃。

這樣,我們列舉懶狗集合,然後根據最後一大條的原則進行轉移,得到了一個 \(O(n4^n)\) 的做法。


但是我們在實現的過程中,發現了 DP 有環的情況,但是我們這個時候並不知道轉移時是否每一個元素都有用,因此環的作用也不知道。

這個怎麼解決呢?

先考慮特殊情況:啥時候會有環。因為看得到的狗是狀態不會變的,變的是自己和自己沒出邊相連的狗。

關於這點,我們很難描述清楚我們的 DP。正難則反,我們交換一下條件,即:

  • 狗狀態變的是自己有出邊的,其他的不變。

很容易發現,這其實是在反圖上操作。

基於這個操作和 DP 的定義,我們很容易給出 DP 有環的另一個定義:在反圖上有環。

  • 因為我們在列舉反圖環上的人時,會讓環上下一個點的狗變成懶狗。這樣一定會有環。

一個很顯然的想法就是環上可以無限操作,但是如果操作環外的點呢?

  • 因為如果我們選擇環外的人,因為是所有情況取 \(\max\),則它得到的答案不會小於 我們不去動環上點 的狀態時的答案。
  • 如果選環上的人,則還可以保持環上有點。
  • 所以即使我們對每個人的時間取 \(\min\),答案還是不小於環上一直有懶狗時的答案。
  • 即答案正無窮大,即無解。

這說明了如果當前狀態環上有懶狗,就整體無解。

同時,縮點成 DAG 後,在圖上按拓撲序歸納證明:

  • 由於轉移中有 \(\max\),對於能通過反圖有向邊到達環上的點,如果這個點上是懶狗,一定會有一個狀態,使得環上有點。

這說明了,如果一個懶狗能通過反圖有向邊到環上,就整體無解。


在反圖上,去掉環和能到達環的點,剩下的圖是個 DAG。

再梳理一遍我們的改變 DP 狀態時的操作:

  • 選擇一個懶狗。
  • 將它變為好狗。
  • 將它出邊連線的任意一些狗變成懶狗。

則我們很容易得到:

  • 在這個 DAG 上,DP 狀態有偏序關係。

如果狀態中沒懶狗,是終止狀態,定義其在第 \(0\) 天會有槍聲。加入這個狀態完美地契合了我們的轉移。

那麼顯然容易歸納證明,只要存在一條懶狗,就不會在第 \(0\) 天有槍聲。

同時因為 DP 的偏序關係,很容易得到,如果對於兩個懶狗集合 \(S, T\)\(S \subset T\),則有 \(f(S) \leq f(T)\)

因為我們在選擇懶狗後,改變狀態後要使 DP 值最大,那麼顯然就是全變成懶狗最好了。

於是操作變成:

  • 選擇一個懶狗。
  • 將它變為好狗。
  • 將它出邊連線的狗全變成懶狗。

由於我們是選擇所有懶狗的最小時間,那麼如果當前狀態中,懶狗 \(u\) 能到達懶狗 \(v (v \neq u)\),那麼先選擇懶狗 \(v\) 是不優的。

於是在每個狀態中:

  • 每次我們只能選擇懶狗 \(u\) 使得不存在懶狗 \(v (v \neq u)\) 能到達 \(u\)
  • 選擇懶狗的次數和初始懶狗集合能到達的所有點的集合大小相等。
    • 即該懶狗集合槍聲時間恰好等於能到達的點集合大小。
  • 選擇槍斃的是在初始懶狗集合中的狗。
  • 選擇槍斃的狗要使時間最小。
    • 即被槍斃的狗,是所有懶狗 \(u\) 使得不存在懶狗 \(v (v \neq u)\) 能到達 \(u\)

這樣,我們得到了一個 \(O(2^n \mathrm{poly}(n))\) 的做法。


當然,都做到這一步了,還能不會多項式做法?

首先取出反圖的 DAG,具體可以直接拓撲排序(選出度為 \(0\) 的點)。當然 tarjan 也可以。

即取出來的子圖點數為 \(m\)

考慮到我們可以直接計算單個狗對答案的貢獻。

對於第一問,對於一個狗 \(u\),我們得出有多少 \(v\) 能到達 \(u\)(包括 \(u\)),這樣:

  • 只要能有點到達 \(u\) 即可。對於其他點,隨便選。如果共 \(x\) 個點能到達 \(u\) (包括 \(u\))。
  • 答案為 \((2^x - 1)2^{m - x}\)

對於第二問,對於一個狗 \(u\),不能有懶狗 \(v\) 能到達它。

  • 記有 \(x\) 個點能到達它(包括 \(u\))。那麼除了自己,其他 \(x - 1\) 個點都不能選。剩下的隨便選。
  • 答案為 \(2^{m-x}\)

用一個傳遞閉包,然後直接統計答案即可。

時間複雜度 \(O(\frac{n^3}{\omega})\)


程式碼有點趕。

#include <bits/stdc++.h>

const int mod = 998244353;
typedef long long LL;
void reduce(int & x) { x += x >> 31 & mod; }
int mul(int a, int b) { return (LL) a * b % mod; }
int pow(int a, int b, int res = 1) {
	for (; b; b >>= 1, a = mul(a, a)) if (b & 1) res = mul(res, a);
	return res;
}
int remod(LL x) { x %= mod; return x + (x >> 63 & mod); }
const int MAXN = 3010;
typedef std::bitset<MAXN> B;
B to[MAXN];
int n;
int oud[MAXN];
int rk[MAXN], in[MAXN];
int main() {
	std::ios_base::sync_with_stdio(false), std::cin.tie(0);
	std::cin >> n;
	for (int i = 1; i <= n; ++i) {
		static char buf[MAXN];
		std::cin >> buf;
		for (int j = 1; j <= n; ++j)
			if (j != i && buf[j - 1] == '0')
				to[i].set(j), ++oud[i];
	}
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j)
			if (!oud[j]) {
				oud[j] = -1;
				for (int k = 1; k <= n; ++k)
					if (to[k][j])
						--oud[k];
				in[rk[i] = j] = true;
				break;
			}
	}
	int m = std::accumulate(in + 1, in + 1 + n, 0);
	for (int i = 1; i <= m; ++i)
		for (int j = 1; j < i; ++j)
			if (to[rk[i]].test(rk[j]))
				to[rk[i]] |= to[rk[j]];
	int ans1 = 0, ans2 = 0;
	for (int i = 1; i <= m; ++i) {
		int u = rk[i];
		int tc = 1;
		for (int j = i + 1; j <= m; ++j)
			tc += to[rk[j]].test(u);
		reduce(ans1 += mul(pow(2, tc) - 1, pow(2, m - tc)) - mod);
		reduce(ans2 += pow(2, m - tc) - mod);
	}
	std::cout << ans1 << ' ' << ans2 << std::endl;
	return 0;
}