1. 程式人生 > 實用技巧 >[USACO18DEC]The Cow Gathering P

[USACO18DEC]The Cow Gathering P

首先可以思考一下每次能刪去的點有什麼性質。

不難發現,每次能刪去的點都是入度恰好為 \(1\) 的那些點(包括 \(a_i \rightarrow b_i\) 的有向邊)。 換句話說,每次能刪去的點既要是樹上的葉子節點,並且不會被任意一條有向邊 \(a_i \rightarrow b_i\) 指向。那麼再來思考一下每個點能否走後離開。

因為 \(i\) 號點只能最後離開,那麼我們將 \(i\) 看作這課樹的根(因為每次只能走葉子)。你會發現如果 \(i\) 不能最後走當前僅當會存在一條路徑(走有向邊) \(x \rightarrow y\) 使得 \(y\)\(x\) 子樹中的點,於是我們單次判斷就能做到 \(O(nm)\)

了。那麼這個判定條件有沒有更為簡單的描述呢?其實是存在的,你會發現如果我們將所有樹邊從兒子指向父親,那麼 \(i\) 不能最後走當且僅當這張有向圖存在著一個環。於是這樣單次判斷的複雜度就能做到 \(O(n + m)\) 了。但這樣的複雜度還不夠,我們可能需要換一種方式思考。

既然每次判斷點不方便,我們能否考慮每條有向邊對每個點的影響呢?事實上是可以的,不難發現對於任意一條有向邊 \(x \rightarrow y\),在以 \(y\) 為根時以 \(x\) 為根的子樹內所有點為根時 \(x \rightarrow y\) 就會在樹上形成一個環,那麼這些點都是不能最後刪除的。那麼我們怎麼找到這些點呢?因為我們顯然不可能每次都換根,可以先欽定 \(1\)

為樹根。那麼你會發現存在兩種情況 \(y\)\(x\) 的祖先時,令 \(f\)\(x \rightarrow y\) 這條鏈上 \(y\) 的兒子(可以 \(O(\log n)\) 倍增求出,在 [USACO19JAN]Exercise Route P 中提到),那麼這些點就會是整棵樹除了以 \(f\) 為根的子樹內的點。那麼我們在根以及 \(f\) 上打標記差分即可。對於其他情況,這些點就會是以 \(x\) 為根的子樹內的點,直接打標記即可。最終我們樹上差分跑一邊 \(dfs\) 即可。

這樣就做完了嗎?事實上並沒有,你會發現你忽略了有向邊之間的影響。那麼怎樣的情況會對答案有影響呢?當且僅當形成了一條跨子樹的路徑 \(x \rightarrow \cdots \rightarrow y\)

其中 \(y\)\(x\) 子樹內的點,並且仔細分析你會發現,如果出現這種情況那麼整張圖是不存在這樣的刪除序列的。那麼判掉是否對答案有貢獻只需判斷這張圖是否有解即可,直接拓撲排序每次加入度數為 \(1\) 的點,最終如果存在沒有入隊的點就無解。

#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
#define Next(i, u) for (int i = h[u]; i; i = e[i].next)
const int N = 100000 + 5;
const int M = 20 + 5;
struct edge {
	int v, next;
}e[N * 3];
bool book[N];
int n, m, u, v, tot, cnt, x[N], y[N], d[N], h[N], c[N], sz[N], dfn[N], dep[N], ans[N], f[N][M];
queue <int> Q;
int read() {
	char c; int x = 0, f = 1;
	c = getchar();
	while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return x * f;
}
void add(int u, int v) {
	e[++tot].v = v, e[tot].next = h[u], h[u] = tot, ++d[v];
}
void dfs(int u, int fa){
	f[u][0] = fa, sz[u] = 1, dfn[u] = ++cnt, dep[u] = dep[fa] + 1;
	Next(i, u) {
		int v = e[i].v; if(v == fa) continue;
		dfs(v, u), sz[u] += sz[v];
	}
}
int find(int x, int y) {
	dep(i, 0, 20) if(dep[f[x][i]] > dep[y]) x = f[x][i];
	return x;
}
void calc(int u, int fa, int tmp){
	ans[u] = tmp + c[u];
	Next(i, u) {
		int v = e[i].v; if(v == fa) continue;
		calc(v, u, tmp + c[u]);
	}
}
int main() {
	n = read(), m = read();
	rep(i, 1, n - 1) u = read(), v = read(), add(u, v), add(v, u);
	dfs(1, 0);
	rep(j, 1, 20) rep(i, 1, n) f[i][j] = f[f[i][j - 1]][j - 1];
	rep(i, 1, m) {
		x[i] = u = read(), y[i] = v = read();
		if(dfn[v] >= dfn[u] && dfn[v] <= dfn[u] + sz[u] - 1) ++c[1], --c[find(v, u)];
		else ++c[u];
	}
	calc(1, 0, 0);
	rep(i, 1, m) add(x[i], y[i]);
	rep(i, 1, n) if(d[i] == 1) Q.push(i), book[i] = true;
	while(!Q.empty()) {
		int u = Q.front(); Q.pop();
		Next(i, u) {
			int v = e[i].v; if(book[v]) continue;
			--d[v]; if(d[v] == 1) Q.push(v), book[v] = 1;
		}
	}
	rep(i, 1, n) if(!book[i]) { 
		rep(j, 1, n) puts("0");
		return 0;
	}
	rep(i, 1, n) printf(ans[i] > 0 ? "0\n" : "1\n");
	return 0;
}

值得一提的是,判定性或定義型問題一定要去思考判定條件。另外,反向考慮每條邊對答案的影響也是非常重要的。當發現自己的做法出現問題或考慮不全的時候,不要慌張,仔細分析看看能否以一種簡單的方式解決這些問題。