【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;
}