2024.3.16 筆記(最短路、LCA、樹上差分、基環樹)
P2868 Sightseeing Cows
題意題目已經說的很清楚了,看到這個題想到了 沙漠之王 那個題,最優比率生成樹。所以直接考慮 01 分數規劃。
二分答案,設二分的值為 \(mid\)
-
- 如果圖中存在一個環 S,使得 \(\sum_{i=1}^t(mid*t[e_i]-f[v_i])<0\),那麼本題所求的最大值一定大於 \(mid\)
-
- 如果對於任意環都有 \(\sum_{i=1}^t(mid*t[e_i]-f[v_i])≥0\),那麼最大值不超過 \(mid\)
綜上所述,對於每輪二分,我們建立一張新圖,結構與原圖相同,但是沒有點權,有向邊 \(e=(x,y)\) 的權值是 \(mid*t[e]-f[x]\)
在這個新的圖上面,\(\sum_{i=1}^t(mid*t[e_i]-f[v_i])<0\) 的含義就是圖中存在負環,因此可以 SPFA
複雜度 \(O(nm\log n)\)
bool SPFA()
{
q.empty();
for (rint i = 1; i <= n; i++)
{
q.push(i);
dist[i] = 0;
cnt[i] = 0;
v[i] = 1;
}
while (!q.empty())
{
int x = q.front();
q.pop();
v[x] = 0;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
double z = w[i];
if (dist[y] > dist[x] + z)
{
dist[y] = dist[x] + z;
cnt[y] = cnt[x] + 1;
if (cnt[y] >= n + 1)
return 1;
if (!v[y])
{
q.push(y);
v[y] = 1;
}
}
}
}
return 0;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++) cin >> fun[i];
for (rint i = 1; i <= m; i++) cin >> a[i].x >> a[i].y >> a[i].time;
double l = 0, r = 1e6;
while (r - l > 1e-4)
{
double mid = (l + r) / 2;
memset(h, 0, sizeof h);
idx = 0;
for (rint i = 1; i <= m; i++) add(a[i].x, a[i].y, mid * a[i].time - fun[a[i].x]);
if (SPFA()) l = mid;
else r = mid;
}
cout << fixed << setprecision(2) << r << endl;
return 0;
}
UVA1723 Intervals
設 \(s[k]\) 表示 \(0\) ~ \(k\) 之間最少選出多少個整數。根據題意,顯然有 \(s[b_i]-s[a_i-1]≥c_i\)
為了保證我們的答案是有意義的,還有一些預設的限制條件要注意到。
-
1.\(s[k]-s[k-1]≥0\)
-
2.\(s[k]-s[k-1]≤1\)
之後直接差分約束就可以了,這個題比較水,程式碼不粘了。
P3629 [APIO2010] 巡邏
不建立新的道路時,從 \(1\) 好節點出發,把整棵樹上的每條邊遍歷至少一次,再返回 \(1\) 一號節點,會恰好經過每條邊 2 次。路線總長度為 \(2(n-1)\)
建立一條新道路之後,因為新道路必須經過恰好一次,所以在沿著新道路 \((x,y)\) 巡邏之後,要返回 \(x\),就必須沿著樹上從 \(y\) 到 \(x\) 的路徑巡邏一遍,最終形成一個環。
因此,當 \(k=1\) 找到樹的最長鏈,在兩個端點之間加上一條新道路,就能讓總的巡邏路徑最小。若樹的直徑為 \(L\) ,答案就是 \(2(n-1)-L+1\)
考慮建立第二條道路,又會形成一個環,如果不重疊顯然答案繼續減小,如果環重疊,讓巡邏車在適當的時候重新巡邏重疊邊,並且返回。
所以這個題只需要求兩次樹的直徑即可。
具體的,在最初的樹上求直徑,設直徑為 \(L_1\),然後把直徑上的邊權取反,在最長鏈的邊權去飯後再次求直徑,設直徑為 \(L_2\)。
答案就是 \(2(n-1)-(L_1-1)-(L_2-1)\)
顯然的,第一次求直徑的時候是要求路徑的,所以考慮 bfs 求,第二次則直接樹形 DP
複雜度 \(O(n)\)
int bfs(int s)
{
memset(d, -1, sizeof d);
q.push(s);
d[s] = 0;
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y] == -1)
{
d[y] = d[x] + 1;
pre[y] = i;
q.push(y);
}
}
}
int p = s;
for (rint i = 1; i <= n; i++)
if (d[i] > d[p])
p = i;
return p;
}
void update(int q, int p)
{
while (q != p)
{
w[pre[q]] = -1;
w[pre[q] ^ 1] = -1;
q = e[pre[q] ^ 1];
}
}
void dp(int x, int father)
{
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (y == father) continue;
dp(y, x);
L2 = max(L2, d[y] + d[x] + w[i]);
d[x] = max(d[x], d[y] + w[i]);
}
}
signed main()
{
cin >> n >> k;
idx = 1;
for (rint i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
int p = bfs(1);
int q = bfs(p);
int L1 = d[q];
int ans = 2 * (n - 1) - L1 + 1;
if (k == 1)
{
cout << ans << endl;
return 0;
}
update(q, p);
memset(d, 0, sizeof d);
dp(1, 0);
cout << 2 * (n - 1) - L1 + 1 - L2 + 1 << endl;
return 0;
}
P1099 [NOIP2007] 樹網的核
這個題資料範圍很小,直接 \(O(n^3)\) 列舉顯然是可以過的,但我們不能僅僅拘泥於此。
暴力做法就是找出直徑然後列舉兩個點。考慮貪心最佳化一下,在樹網的核的一端 \(p\) 固定後,另一端 \(q\) 在距離不超過 \(s\) 的前提下,顯然越遠越好。因此,我們只需在直徑上列舉 \(p\),然後直接確定 \(q\) 的位置,再深度優先遍歷即可,複雜度可以達到 \(O(n^2)\)
但是我們還想再快一點。
設直徑上的節點為 \(u_1,u_2....\),把這幾個節點標記為已訪問,然後透過深度優先遍歷,求出 \(d[u_i]\),表示從 \(u_i\) 出發,不經過直徑上的其他節點,能夠到達的最短點的距離。
以 \(u_i,u_j\) 為端點的樹網的核的偏心距就是:
用單調佇列維護已經可以 \(O(n)\) 了,但是可以繼續最佳化,式子可以簡化為 \(max(max_{1≤k≤t}d[u_k],dist(u1,u_i),dist(u_j,u_t))\)。對於這個式子,只需要列舉直徑上的每個點 \(u_i\) 雙指標更新答案即可。
int bfs(int s)
{
memset(d, -1, sizeof d);
q.push(s);
d[s] = 0;
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y] == -1)
{
d[y] = d[x] + w[i];
pre[y] = i;
q.push(y);
}
}
}
int p = s;
for (rint i = 1; i <= n; i++)
if (d[i] > d[p])
p = i;
return p;
}
void update(int q, int p)
{
while (q != p)
{
a[++t] = q;
b[t + 1] = w[pre[q]];
q = e[pre[q] ^ 1];
}
}
void dfs(int x)
{
v[x] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (v[y]) continue;
dfs(y);
f[x] = max(f[x], f[y] + w[i]);
}
}
signed main()
{
cin >> n >> s;
idx = 1;
for (rint i = 1; i < n; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
int p = bfs(1);
int q = bfs(p);
update(q, p);
a[++t] = p;
for (rint i = 1; i <= t; i++) v[a[i]] = 1;
int maxf = 0;
for (rint i = 1; i <= t; i++)
{
dfs(a[i]);
maxf = max(maxf, f[a[i]]);
sum[i] = sum[i - 1] + b[i];
}
int ans = inf;
for (rint i = 1, j = 1; i <= t; i++)
{
while (j < t && sum[j + 1] - sum[i] <= s) j++;
ans = min(ans, max(maxf, max(sum[i], sum[t] - sum[j])));
}
cout << ans << endl;
return 0;
}
AcWing355. 異象石
對整棵樹進行 \(dfs\),求出每個點的時間戳,發現如果按照時間戳從小到大的順序,把節點排成一圈(首尾相連),累加相鄰兩個節點之間的路徑長度,最後得到的結果恰好是所求答案的兩倍。
因此可以用一個資料結構 set
按照時間戳遞增的順序維護出現異象石的節點序列,並用變數 \(ans\) 記錄序列中相鄰兩個節點之間的
路徑長度之和(序列首尾也是相鄰的)
設 \(path(x, y)\) 表示樹上 \(x, y\) 之間的路徑長度,設 \(dist[x]\) 表示 \(x\) 到根節點的路徑長度
那麼 \(path(x, y) = dist[x] + dist[y] - 2 * (dist[lca(x, y)]),\) \(dist\) 用 \(dfs\) 求
\(path(x, y)\) 可以 \(LCA\)
若一個節點出現了異象石,就依據時間戳,把它插入上述節點序列中適當的位置,設插入的節點為 \(x\),它在序列中前後分別是節點 \(l\) 和 \(r\),我們就讓 \(ans\) 減去 \(path(l, r)\),加上 \(path(l, x) + path(x, r)\)。
若一個節點的異象石被摧毀,則讓 \(ans\) 減去$ path(l, x) + path(x, r)$,加上 \(path(l, r)\)
對於每一個詢問直接輸出 \(ans\) 即可。
複雜度 \(O((N+M)\log N)\)
void bfs()
{
q.push(1);
v[1] = 1;
d[1] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (v[y]) continue;
v[y] = 1;
d[y] = d[x] + 1;
fa[y][0] = x;
dist[y][0] = w[i];
for (rint j = 1; j <= 20; j++)
{
fa[y][j] = fa[fa[y][j - 1]][j - 1];
dist[y][j] = dist[fa[y][j - 1]][j - 1] + dist[y][j - 1];
}
q.push(y);
}
}
}
int lca(int x, int y)
{
int ans = 0;
if (d[x] > d[y]) swap(x, y);
for (rint i = 20; i >= 0; i--)
if (d[fa[y][i]] >= d[x])
ans += dist[y][i], y = fa[y][i];
if (x == y) return ans;
for (rint i = 20; i >= 0; i--)
if (fa[x][i] != fa[y][i])
ans += dist[x][i] + dist[y][i], x = fa[x][i], y = fa[y][i];
return ans + dist[x][0] + dist[y][0];
}
void dfs(int x)
{
v[x] = ++idx;
a[idx] = x;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (v[y]) continue;
dfs(y);
}
}
auto L(auto it)
{
if (it == s.begin()) return --s.end();
return --it;
}
auto R(auto it)
{
if (it == --s.end()) return s.begin();
return ++it;
}
signed main()
{
cin >> n;
for (rint i = 1; i < n; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
bfs();
memset(v, 0, sizeof v);
idx = 0;
dfs(1);
cin >> m;
for (rint i = 1; i <= m; i++)
{
scanf("%s", str);
if (str[0] == '+')
{
int x;
cin >> x;
if (s.size())
{
auto it = s.lower_bound(v[x]);
if (it == s.end()) it = s.begin();
int y = *L(it);
ans += lca(x, a[y]) + lca(x, a[*it]) - lca(a[y], a[*it]);
}
s.insert(v[x]);
}
if (str[0] == '-')
{
int x;
cin >> x;
auto it = s.find(v[x]);
int y = *L(it);
it = R(it);
ans -= lca(x, a[y]) + lca(x, a[*it]) - lca(a[y], a[*it]);
s.erase(v[x]);
}
if (str[0] == '?')
{
cout << ans / 2 << endl;
}
}
return 0;
}
P4180 嚴格次小生成樹
先求出任意一顆最小生成樹。設邊權之和為 \(sum\),我們稱在這棵最小生成樹中的 \(n-1\) 條邊為樹邊。其他 \(m-n+1\) 條邊為非樹邊。
把一條豎邊 \((x,y,z)\) 新增到最小生成樹中,會與樹上 \((x,y)\) 之間的路徑一起形成一個環,設樹上 \(x,y\), 之間的路徑上的邊權最大為 \(val_1\),嚴格次大權為 \(val_2\)
若 \(z>val_1\) 則把 \(val_1\) 對應的那條邊替換成 \((x,y,z)\) 這條邊,就得到了嚴格次小生成數的一個候選答案,邊之和為 \(sum-val_1+z\)
若 \(z=val_1\),則把 \(val_2\) 對應的那條邊替換成 \((x,y,z)\) 這條邊,就得到了嚴格思想生成數的一個候選答案,邊選之和為 \(sum-val_2+z\)
列舉每條非樹邊,新增到最小生成樹中計算出上述所有候選答案,在候選答案中取最小值就得到了整張無向圖的嚴格次小生成樹,因此我們要解決的主要問題是如何快速求出一條路徑上的最大邊權與嚴格次大邊權
設 \(f[x,k]\) 表示 \(x\) 的 \(2_k\) 輩祖先,\(g[x,k,0/1]\) 表示從 \(x\) 到 \(f[x,k]\) 的路徑上的最大邊權和嚴格次大邊權。對於 \(∀k∈[1,\log n]\) 有
PS:上式中 \(k_1,k_2\) 的取值隨情況改變。
複雜度 \(O(M \log N)\)
struct rec
{
int x, y, z;
bool k;
friend bool operator < (rec a, rec b)
{
return a.z < b.z;
}
} p[M];
int n, m;
int fa[N], d[N], f[N][21];
int g[N][21][2], sum, ans = inf;
vector<pair<int, int>> e[N];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void kruskal()
{
sort(p + 1, p + m + 1);
for (rint i = 1; i <= n; i++) fa[i] = i;
for (rint i = 1; i <= m; i++)
{
int x = find(p[i].x);
int y = find(p[i].y);
if (x == y) continue;
fa[x] = y;
sum += p[i].z;
p[i].k = 1;
}
}
void dfs(int x)
{
for (rint i = 0; i < e[x].size(); i++)
{
int y = e[x][i].x;
if (d[y]) continue;
d[y] = d[x] + 1;
f[y][0] = x;
int z = e[x][i].y;
g[y][0][0] = z;
g[y][0][1] = -inf;
for (rint j = 1; j <= 20; j++)
{
f[y][j] = f[f[y][j - 1]][j - 1];
g[y][j][0] = max(g[y][j - 1][0], g[f[y][j - 1]][j - 1][0]);
if (g[y][j - 1][0] == g[f[y][j - 1]][j - 1][0]) g[y][j][1] = max(g[y][j - 1][1], g[f[y][j - 1]][j - 1][1]);
else if (g[y][j - 1][0] < g[f[y][j - 1]][j - 1][0]) g[y][j][1] = max(g[y][j - 1][0], g[f[y][j - 1]][j - 1][1]);
else g[y][j][1] = max(g[y][j - 1][1], g[f[y][j - 1]][j - 1][0]);
}
dfs(y);
}
}
void lca(int x, int y, int &val1, int &val2)
{
if (d[x] > d[y]) swap(x, y);
for (rint i = 20; i >= 0; i--)
if (d[f[y][i]] >= d[x])
{
if (val1 > g[y][i][0]) val2 = max(val2, g[y][i][0]);
else
{
val1 = g[y][i][0];
val2 = max(val2, g[y][i][1]);
}
y = f[y][i];
}
if (x == y) return ;
for (rint i = 20; i >= 0; i--)
if (f[x][i] != f[y][i])
{
val1 = max(val1, max(g[x][i][0], g[y][i][0]));
val2 = max(val2, g[x][i][0] != val1 ? g[x][i][0] : g[x][i][1]);
val2 = max(val2, g[y][i][0] != val1 ? g[y][i][0] : g[y][i][1]);
x = f[x][i];
y = f[y][i];
}
val1 = max(val1, max(g[x][0][0], g[y][0][0]));
val2 = max(val2, g[x][0][0] != val1 ? g[x][0][0] : g[x][0][1]);
val2 = max(val2, g[y][0][0] != val1 ? g[y][0][0] : g[y][0][1]);
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= m; i++)
{
cin >> p[i].x >> p[i].y >> p[i].z;
p[i].k = 0;
}
kruskal();
for (rint i = 1; i <= m; i++)
if (p[i].k)
{
e[p[i].x].push_back(make_pair(p[i].y, p[i].z));
e[p[i].y].push_back(make_pair(p[i].x, p[i].z));
}
d[1] = 1;
for (rint i = 0; i <= 20; i++) g[1][i][0] = g[1][i][1] = -inf;
dfs(1);
for (rint i = 1; i <= m; i++)
{
if (!p[i].k)
{
int val1 = -inf, val2 = -inf;
lca(p[i].x, p[i].y, val1, val2);
if (p[i].z > val1) ans = min(ans, sum - val1 + p[i].z);
else ans = min(ans, sum - val2 + p[i].z);
}
}
cout << ans << endl;
return 0;
}
P1084 [NOIP2012] 疫情控制
顯然有一個結論,軍隊所在的節點深度越淺,能管轄的葉子節點越多,所以可以二分答案。判定本題的答案滿足單調性,因此考慮二分答案,把問題轉化為判定二分的值,該時間內是否能控制疫情。
軍隊往上爬時,可運用類似 \(LCA\) 的倍增方法,先做預處理,再按二進位制位從大到小列舉。
對於每一棵子樹而言,設其到跟的距離為 \(d\)。
若其不能用自身的軍隊進行控制,或所有部分軍隊都到達根節點使其無法控制,那麼他就需要幫助:
第一類點是這顆子樹內部到達根節點的,則直接返回即可。
第二類是其他子樹在 \(lim\) 限制下有多餘時間,多餘時間必須大於等於 \(d\) 。
對於每一棵子樹,判斷第一類的最小剩餘是否小於等於 \(d\) :
若是,則說明這一點是第一類和第二類中多餘時間最少的,貪心取即可。
否則,在之後掃描過程中選取大於等於 \(d\) 的最小值即可。
掃描過程可直接對可用軍隊和需要的子樹分別按時間從小到大排序。
複雜度 \(O(\log( \sum w ) n\log n)\)
void bfs()
{
d[1] = 1;
q.push(1);
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!d[y])
{
q.push(y);
d[y] = d[x] + 1;
dist[y] = dist[x] + w[i];
fa[y][0] = x;
for (rint k = 1; k <= 17; k++)
fa[y][k] = fa[fa[y][k - 1]][k - 1];
}
}
}
}
pair<int, int> calc(int x, int mid)
{
for (rint i = 20; i >= 0; i--)
{
if (fa[x][i] > 1 && dist[x] - dist[fa[x][i]] <= mid)
{
mid -= dist[x] - dist[fa[x][i]];
x = fa[x][i];
}
}
return make_pair(mid, x);
}
void dfs(int x)
{
bool all_child_covered = 1;
bool is_leaf = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y] <= d[x]) continue;
dfs(y);
all_child_covered &= cover[y];
is_leaf = 0;
if (x == 1 && !cover[y]) son[++p] = y;
}
cover[x] = has[x] || (!is_leaf && all_child_covered);
}
bool cmp(int x, int y)
{
return dist[x] < dist[y];
}
bool solve(int mid)
{
memset(has, 0, sizeof has);
memset(cover, 0, sizeof cover);
memset(used, 0, sizeof used);
cnt = p = 0;
for (rint i = 1; i <= m; i++)
{
pair<int, int> k = calc(army[i], mid);
int rest = k.x;
int pos = k.y;
if (rest <= dist[pos]) has[pos] = 1; // 一類軍隊
else a[++cnt] = make_pair(rest - dist[pos], pos); // 二類軍隊(減去到根的時間)
}
dfs(1);
sort(a + 1, a + cnt + 1);
for (rint i = 1; i <= cnt; i++)
{
int rest = a[i].x;
int s = a[i].y;
if (!cover[s] && rest < dist[s])
cover[s] = used[i] = 1; // 上去就下不來了,就不要上去
}
sort(son + 1, son + p + 1, cmp);
for (rint i = 1, j = 1; i <= p; i++)
{
int s = son[i];
if (cover[s]) continue;
while (j <= cnt && (used[j] || a[j].x < dist[s])) j++;
if (j > cnt) return 0;
j++; // 用j管轄s
}
return 1;
}
signed main()
{
cin >> n;
for (rint i = 1; i < n; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
r += c;
}
bfs();
cin >> m;
for (rint i = 1; i <= m; i++)
{
cin >> army[i];
}
while (l < r)
{
int mid = (l + r) >> 1;
if (solve(mid)) r = mid;
else l = mid + 1;
}
cout << l << endl;
return 0;
}
AcWing352. 闇の連鎖
在沒有附加邊的情況下,我們發現這是一顆樹,那麼再新增條附加邊 \((x,y)\) 後,會造成 \((x,y)\) 之間產生一個環
如果我們第一步截斷了 \((x,y)\) 之間的一條路,那麼我們第二次只能截掉 \((x,y)\) 之間的附加邊,才能使其不連通;
我們將每條附加邊 \((x,y)\) 稱為將 \((x,y)\) 之間的路徑覆蓋了一遍;
因此我們只需要統計出每條主要邊被覆蓋了幾次即可;
對於只被覆蓋一次的邊,第二次我們只能切斷 \((x,y)\) 邊,方法唯一;
如果我們第一步切斷了被覆蓋0次的邊,那麼我們已經將其分為兩部分,那麼第二部只需要在m條附加邊中任選一條即可,如果第一步截到被覆蓋超過兩次的邊,將無法將其分為兩部分;
運用乘法原理,我們累加答案;
那麼怎麼標記我們的邊 \((x,y)\) 被覆蓋了幾次呢,那麼我們可以使用樹上差分,是解決此類問題的經典套路;
我們想,對於一條邊 \((x,y)\) ,我們新增一條邊;
那麼只會對 \(x\) 到 \(lca(x,y)\) 到 \(y\) 上的邊產生影響,對於 \((x,y)\) 我們將 \(x\) 節點的權值 \(+1\),\(y\) 節點的權值 \(+1\),另 \(lca (x,y)\) 的權值 \(-2\),畫圖很好理解,那麼我們進行一遍 \(dfs\) 求出每個節點權值,那麼這個值就是節點父節點連邊被覆蓋的次數,按上述方法累加答案即可;
時間複雜度分析:\(O(N+M)\)
void bfs()
{
q.push(1);
d[1] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i =ne[i])
{
int y = e[i];
if (d[y]) continue;
d[y] = d[x] + 1;
fa[y][0] = x;
for (rint j = 1; j <= 20; j++)
fa[y][j] = fa[fa[y][j - 1]][j - 1];
q.push(y);
}
}
}
int lca(int x, int y)
{
if (d[x] > d[y]) swap(x, y);
for (rint i = 20; i >= 0; i--)
if (d[fa[y][i]] >= d[x])
y = fa[y][i];
for (rint i = 20; i >= 0; i--)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
if (x == y) return x;
return fa[x][0];
}
// dfs 返回每一棵子樹的和
int dfs(int x, int father)
{
// 遍歷以u為根節點的子樹j的和
int res = f[x];
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (y == father) continue;
// 邊t→j 砍掉後的方案 s
int s = dfs(y, x);
// 如果s=0 則隨便砍
if (s == 0) ans += m;
// 如果s=1 則只能砍對應的非樹邊
else if (s == 1) ans++;
// 子節點j的差分向上加給/傳給 節點u
res += s;
}
// 如果沒有子節點 即葉子節點 直接返回d[node]
return res;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
bfs();
// 讀入附加邊==非樹邊
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
int p = lca(a, b);
f[a]++, f[b]++, f[p] -= 2;
}
dfs(1, 1);
cout << ans << endl;
return 0;
}
P4556 雨天的尾巴
要想求出每個點存放最多的是哪種型別的物品,需要求出每個點上存放的每種物品的數量。
樸素做法,對物品的型別進行離散化(最多 \(M\) 種不同物品),然後對每個點 \(x\) 建立一個計數陣列 \(c[x][1\)~\(M]\)
依次執行每個發放操作,對 \(x\) 到 \(y\) 的路徑上的每個點 \(p\),令 \(c[p][z]\) 加 \(1\),最終掃描計數陣列得到答案。
但是這樣肯定超時,因此需要最佳化,為了避免遍歷從 \(x\) 到 \(y\) 的路徑,我們可以使用樹上差分,對於每條從 \(x\) 到 \(y\) 的路徑上發放 \(z\),使 \(c[x][z] + 1\),使 \(c[y][z] + 1\),由於路徑上所有點都 \(+ 1\),包括 \(x\) 和 \(y\) 的最近公共祖先,因此需要使 \(c[lca(x, y)][z] - 1\),使 \(c[father(lca(x, y))][z] - 1\)
為了節省空間並快速使兩個計數陣列相加,可以使用線段樹合併,對於每個點建立一個動態開點的線段樹,來代替差分陣列,動態的維護最大值以及最大值對應的型別,然後深搜求子樹和,每次的求和等價於每兩個線段樹合併,最終得出每個點的答案。
複雜度為 \(O((N+M)\log (N+M))\)
struct node
{
int l, r;
int dat, pos;
} t[M];
int n, m;
int f[N][21], d[N], root[N], ans[N];
int e[M], ne[M], h[N], idx;
int X[N], Y[N], Z[N], val[N];
int num, cnt;
queue<int> q;
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void bfs()
{
q.push(1);
d[1] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y]) continue;
d[y] = d[x] + 1;
f[y][0] = x;
for (rint j = 1; j <= 20; j++)
f[y][j] = f[f[y][j - 1]][j - 1];
q.push(y);
}
}
}
int lca(int x, int y)
{
if (d[x] > d[y]) swap(x, y);
for (rint i = 20; i >= 0; i--)
if (d[f[y][i]] >= d[x])
y = f[y][i];
if (x == y) return x;
for (rint i = 20; i >= 0; i--)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
}
void insert(int p, int l, int r, int val, int delta)
{
if (l == r)
{
t[p].dat += delta;
t[p].pos = t[p].dat ? l : 0;
return ;
}
int mid = (l + r) >> 1;
if (val <= mid)
{
if (!t[p].l) t[p].l = ++num;
insert(t[p].l, l, mid, val, delta);
}
else
{
if (!t[p].r) t[p].r = ++num;
insert(t[p].r, mid + 1, r, val, delta);
}
t[p].dat = max(t[t[p].l].dat, t[t[p].r].dat);
t[p].pos = t[t[p].l].dat >= t[t[p].r].dat ? t[t[p].l].pos : t[t[p].r].pos;
}
int merge(int p, int q, int l, int r)
{
if (!p) return q;
if (!q) return p;
if (l == r)
{
t[p].dat += t[q].dat;
t[p].pos = t[p].dat ? l : 0;
return p;
}
int mid = (l + r) >> 1;
t[p].l = merge(t[p].l, t[q].l, l, mid);
t[p].r = merge(t[p].r, t[q].r, mid + 1, r);
t[p].dat = max(t[t[p].l].dat, t[t[p].r].dat);
t[p].pos = t[t[p].l].dat >= t[t[p].r].dat ? t[t[p].l].pos : t[t[p].r].pos;
return p;
}
void dfs(int x)
{
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y] <= d[x]) continue;
dfs(y);
root[x] = merge(root[x], root[y], 1, cnt);
}
ans[x] = t[root[x]].pos;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
bfs();
for (rint i = 1; i <= n; i++) root[i] = ++num;
for (rint i = 1; i <= m; i++)
{
cin >> X[i] >> Y[i] >> Z[i];
val[i] = Z[i];
}
sort(val + 1, val + m + 1);
cnt = unique(val + 1, val + m + 1) - val - 1;
for (rint i = 1; i <= m; i++)
{
int x = X[i], y = Y[i];
int z = lower_bound(val + 1, val + cnt + 1, Z[i]) - val;
int p = lca(x, y);
insert(root[x], 1, cnt, z, 1);
insert(root[y], 1, cnt, z, 1);
insert(root[p], 1, cnt, z, -1);
if (f[p][0]) insert(root[f[p][0]], 1, cnt, z, -1);
}
dfs(1);
for (rint i = 1; i <= n; i++) cout << val[ans[i]] << endl;
return 0;
}
P1600 [NOIP2016] 天天愛跑步
對於每個玩家的跑步路線 \(s -> t\),可以拆成兩端:\([s -> lca(s, t)]\),\((lca(s, t) -> t]\)
因此可以發現,對於節點 \(x\) 能觀察到第 \(i\) 個玩家,只需滿足以下兩個條件之一即可。
-
節點 \(x\) 處於 \([s -> lca(s, t)]\) 路徑上,且滿足 \(dep[s] - dep[x] = w[x]\) (\(dep[]\) 表示每個節點的深度)
意味著玩家從 \(s\) 跑到 \(x\) 所用的時間為 \(dep[s] - dep[x]\),即節點 \(x\) 觀察的時間 \(w[x]\)。等價於節點 $x $
能觀察到所有在樹上的深度為 \(dep[x] + w[x]\) 的玩家 -
節點 \(x\) 處於 \((lca(s, t] -> t]\) 路徑上,且滿足 \(dep[s] + dep[t] - 2 * (dep[lca(s, t)]) = w[x]\),
意味著玩家從 \(s\) 跑到 \(x\) 所用的時間為 \(dep[s] + dep[t] - 2 * (dep[lca(s, t)])\),即節點 \(x\) 觀察的時間 $ w[x]$。
等價於節點 \(x\) 能觀察到所有在樹上的深度為 \(w[x] + 2 * (dep[lca(s, t)]) - dep[t]\) 的玩家
以上兩個條件包含對 \(x\) 所在路徑限制且互不重疊,因此可以分開計算滿足每個條件的玩家數量,對於節點 \(x\),能被它觀察到的玩家在樹上的深度只能為 \(dep[x] + w[x] 和 w[x] + 2 * (dep[lca(s, t]) - dep[t]\),因此只需要累加上這兩個深度的玩家數量即可。
這裡以 \([s -> lca(s, t)]\) 即條件一為例。
對於如何快速的累加和統計每個節點上能觀察到的玩家數量,首先將每個深度看作一個型別,對於每個玩家 \(i\),
我們可以在 \(s -> t\) 路徑上的每個點增加一個 \(dep[s]\) 型別的物品,對於每個節點 \(x\) 能觀察到的玩家數量就是:
節點 \(x\) 上 型別為 \(dep[x] + w[x]\) 的物品個數 \(+\) 型別為 \(w[x] + 2 * (dep[lca(s, t]) - dep[t]\) 的物品個數。
這裡可以採用樹上差分的方式來快速累加每個節點上每個型別的物品數量,對於第 \(i\) 個玩家,使節點 \(s\) 處型別為 \(dep[s]\) 的物品個數 \(+ 1\),使節點 \(father(lca(s, t))\) 處型別為 \(dep[s]\) 的物品個數 \(- 1\)
然後每個節點上都需要維護若干個型別的物品個數,這裡可以用動態開點的線段樹來維護,求子樹和時只需要合併線段樹。但是本題只需要維護每個節點上特定型別的物品個數,因此可以用更簡單的方式,對每個節點建立一個 \(vector\) 容器,掃描 \(m\) 個玩家,把每個玩家的增加和減去的物品型別記錄在對應節點的 \(vector\) 中。
建立一個計數陣列 \(c[]\),對每個型別的物品進行計數。
對整棵樹進行 \(dfs\),在遞迴進入每個節點 \(x\) 時,用一個 \(cnt\) 記錄 \(c[w[x] + dep[x]]\),然後掃描節點 \(x\) 的 \(vector\),在計數陣列中執行修改(型別為 \(z\) 的物品對應的進行 \(+1/-1\) 操作),繼續遞迴遍歷所有子樹,在從節點 \(x\) 回溯之前,以節點 \(x\) 為根節點的子樹和就是 \(c[w[x] + dep[x]] - cnt\),即節點 \(x\) 處型別為 \(w[x] + dep[x]\) 的物品數量。
對於條件二,只需改為物品 \(dep[s] - 2 * dep[lca()]\) 在節點 \(t\) 處的數量 \(+ 1\),在節點 \(lca(s, t)\) 處的數量 \(- 1\).
最後求每個節點 \(x\) 處型別為 \(w[x] - dep[x]\) 的物品數量,注意此時物品型別可能是負數,因此可以使用離散化或加一個偏移量。最後兩個條件得到的結果相加,就是節點 \(x\) 能觀察到的玩家數量
複雜度 \(O(n \log n)\)
void bfs()
{
q.push(1);
d[1] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (d[y]) continue;
d[y] = d[x] + 1;
f[y][0] = x;
for (rint j = 1; j <= 20; j++)
f[y][j] = f[f[y][j - 1]][j - 1];
q.push(y);
}
}
}
int lca(int x, int y)
{
if (d[x] > d[y]) swap(x, y);
for (rint i = 20; i >= 0; i--)
if (d[f[y][i]] >= d[x])
y = f[y][i];
if (x == y) return x;
for (rint i = 20; i >= 0; i--)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
}
void dfs(int x)
{
int val1 = c1[d[x] + w[x]];
int val2 = c2[w[x] - d[x] + n];
v[x] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (v[y]) continue;
dfs(y);
}
for (rint i = 0; i < a1[x].size(); i++) c1[a1[x][i]]++;
for (rint i = 0; i < b1[x].size(); i++) c1[b1[x][i]]--;
for (rint i = 0; i < a2[x].size(); i++) c2[a2[x][i] + n]++;
for (rint i = 0; i < b2[x].size(); i++) c2[b2[x][i] + n]--;
ans[x] += c1[d[x] + w[x]] - val1 + c2[w[x] - d[x] + n] - val2;
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
for (rint i = 1; i <= n; i++) cin >> w[i];
bfs();
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
int c = lca(a, b);
a1[a].push_back(d[a]);
b1[f[c][0]].push_back(d[a]);
a2[b].push_back(d[a] - 2 * d[c]);
b2[c].push_back(d[a] - 2 * d[c]);
}
dfs(1);
for (rint i = 1; i <= n; i++) cout << ans[i] << " ";
return 0;
}
P4381 Island
本題的圖是一個 \(n\) 個點 \(n\) 條邊的圖,由於不保證整張圖連通,因此根據定義可以知道這是一個基環樹森林。
基環樹森林中每一個連通塊都是一個單獨的基環樹,根據渡船的規則可知一旦離開一顆基環樹,就不能再渡船回來。
因此只需要對於每棵基環樹內部,求一個最長簡單路徑,即基環樹的直徑。
所以本題的答案就是所有基環樹的直徑之和。
剩下的問題就是如何求出每棵基環樹的直徑。
首先對於每棵基環樹的直徑都能分成兩種情況:
- 在去掉環之後的某棵子樹中。
- 經過環,其兩端分別在去掉環上所有邊之後的兩棵不同子樹中。
先用 \(dfs\) 找出基環樹的 環,把 環 上的節點做上標記,設環上的節點為 \(s[1], s[2], ..., s[t]\)
從每個 \(s[i]\) 出發,在不經過環上其他節點的前提下,再次執行 dfs,訪問去掉 環 之後以 \(s[i]\) 為根的子樹,
在這樣的每棵子樹中,按照求樹的直徑的方法進行 樹型 dp 並更新答案,即可處理第一種情況,同時還可以計算出 \(d[s[i]],\)
表示從節點 \(s[i]\) 出發走向以 \(s[i]\) 為根的子樹,能夠到達的最遠節點的距離。
最後,考慮第二種情況,相當於找到環上兩個不同的節點 \(s[i]\), \(s[j]\),使得 \(d[s[i]] + d[s[j]] + dist(s[i], s[j])\) 最大。
其中 \(dist(s[i], s[j])\) 表示 \(s[i], s[j]\) 在環上的距離,有逆時針、順時針兩種走法,取較長的一種,可以將環斷開成鏈在複製一遍,用單調佇列來求。
// s1, s2, ..., sp即為環上點
void get_cycle(int x, int y, int z)
{
sum[1] = z;
while (y != x)
{
s[++p] = y;
sum[p + 1] = w[fa[y]];
y = e[fa[y] ^ 1];
}
s[++p] = x;
// 環斷開,複製一遍
for (rint i = 1; i <= p; i++)
{
v[s[i]] = 1;
s[p + i] = s[i];
sum[p + i] = sum[i];
}
for (rint i = 1; i <= 2 * p; i++) sum[i] += sum[i - 1];
}
void dfs(int x)
{
dfn[x] = ++num;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
fa[y] = i;
dfs(y);
}
else if ((i ^ 1) != fa[x] && dfn[y] > dfn[x])
get_cycle(x, y, w[i]);
}
}
void dp(int x)
{
v[x] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!v[y])
{
dp(y);
ans = max(ans, d[x] + d[y] + w[i]);
d[x] = max(d[x], d[y] + w[i]);
}
}
}
signed main()
{
cin >> n;
idx = 1;
for (rint i = 1; i <= n; i++)
{
int a, b;
cin >> a >>b;
add(i, a, b);
add(a, i, b);
}
for (rint i = 1; i <= n; i++)
{
if (!dfn[i])
{
p = 0;
ans = 0;
dfs(i);
for (rint i = 1; i <= p; i++) dp(s[i]);
int l = 1, r = 0;
for (rint i = 1; i <= 2 * p; i++)
{
while (l <= r && q[l] <= i - p) l++;
if (l <= r) ans = max(ans, d[s[i]] + d[s[q[l]]] + sum[i] - sum[q[l]]);
while (l <= r && d[s[q[r]]] - sum[q[r]] <= d[s[i]] - sum[i]) r--;
q[++r] = i;
}
ans_tot += ans;
}
}
cout << ans_tot << endl;
return 0;
}
AcWing359. 創世紀
由於每個點可以限制另外一個點,如果我們看作從每個點向他限制的點連邊,則本題就是一個基環森林。我們就是要在一個基環森林中選儘可能多的點,使得每個點都能被某一個沒選中的點限制。
由於基環森林中每一棵基環樹都是相互獨立的,因此我們可以單獨考慮每一棵基環樹中最多能選擇多少個點。
我們先考慮如果在一棵普通的樹中如何求,一個點被限制就意味著有一個點指向它,因此就是要保證每個點的父節點都不能被選擇,這樣的方案我們可以用樹形 \(dp\) 來求。
設 \(f_{u,0}\) 表示從以 \(u\) 為根的子樹中選若干個點,且不選 \(u\) 的所有方案的最大值。設 \(f_{u,1}\) 表示從以 \(u\) 為根的子樹中選若干個點,且選擇 \(u\) 的所有方案的最大值。
設 \(u\) 的每個父節點為 \(s\),可得狀態轉移方程:
可以發現這裡的樹形 \(dp\) 中每個點的狀態是透過父節點遞推過來的,因此我們需要建一個反向邊,反向遞推。
但是基環樹是沒法做樹形 \(dp\) 的,因此我們可以將環上某一條邊斷開,這樣基環樹就能變成一棵普通的樹,就能做樹形 \(dp\) 了,我們取環上一點 \(p\),將 \(p \rightarrow A_p\) 的邊斷開,此時我們可以將所有方案分成兩類,一類是不用 \(p \rightarrow A_p\) 這條邊,這就意味著要麼不選 \(A_p\),要麼選 \(A_p\) 並且有另外一條邊指向 \(A_p\)。此時沒有用到 \(p \rightarrow A_p\) 這條邊,所以對 \(p\) 沒有任何限制,我們從 \(p\) 開始求一遍樹形 \(dp\),最終得到兩個狀態 \(f_{p,0}\) 和 \(f_{p,1}\),由於 \(p\) 選不選都行,因此這一類的答案就是這兩個狀態取一個最大值。
另一類是用 \(p \rightarrow A_p\),這意味著我們一定要選 \(A_p\),且一定不選 \(p\),那麼我們只需要在做樹形 \(dp\) 的過程中特判一下 \(A_p\) 一定要選即可,最終同樣得到兩個狀態 \(f_{p,0}\) 和 \(f_{p,1}\),由於 \(p\) 此時一定不能選,因此這一類的答案就是 \(f_{p,0}\)
最終再從這兩類情況的答案中取一個最大值,就是整個的答案。
注意,本題按照每個點向它限制的點連邊的話,會得到一棵內向樹,由於內向樹有多個入度為 \(0\) 的點,而樹形 \(dp\) 需要從一個根節點往下遞迴,因此這裡需要建反向邊,此時將 \(p \rightarrow A_p\) 這條邊刪掉後,\(p\) 就是根節點,我們就可以從 \(p\) 進行遞推,而前面我們分析樹形 \(dp\) 時也分析到需要建反向邊,這裡一舉兩得。
綜上,複雜度 \(O(n)\)
void get_cycle(int x, int y, int i)
{
if (a[x] == y) root = x; // x-->y
else root = y; // y-->x
br = i;
}
void dfs(int x)
{
dfn[x] = ++num;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
fa[y] = i;
dfs(y);
}
else if ((i ^ 1) != fa[x] && dfn[y] >= dfn[x])
// 加上等於號處理自環
get_cycle(x, y, i);
}
}
void dp(int x, int times)
{
f[x][0] = f[x][1] = 0;
v[x] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!v[y] && i != br && (i ^ 1) != br)
{
dp(y, times);
f[x][0] += max(f[y][0], f[y][1]);
}
}
if (times == 2 && x == a[root])
{
f[x][1] = f[x][0] + 1;
}
else
{
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!v[y] && i != br && (i ^ 1) != br)
f[x][1] = max(f[x][1], f[y][0] + f[x][0] - max(f[y][0], f[y][1]) + 1);
}
}
v[x] = 0;
}
signed main()
{
cin >> n;
idx = 1;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
add(i, a[i]);
add(a[i], i);
}
for (rint i = 1; i <= n; i++)
{
if (!dfn[i])
{
dfs(i);
dp(root, 1);
int ans = max(f[root][0], f[root][1]);
dp(root, 2);
ans = max(ans, f[root][0]);
ans_tot += ans;
}
}
cout << ans_tot << endl;
return 0;
}
AcWing360. Freda的傳呼機
懶得寫了,累了,留個程式碼算了。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 2e4 + 5;
const int M = 1e5 + 5;
int n, m, t, num, cnt;
int e[M], ne[M], w[M], h[N], idx;
int d[N], dist[N], f[N][21], dfn[N], fa[N], sum[N], in[N], len_cycle[N];
bool br[M], v[N];
queue<int> q;
void add(int a, int b, int c)
{
e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}
void spfa()
{
memset(dist, 0x7f, sizeof dist);
dist[1] = 0;
q.push(1);
v[1] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
v[x] = 0;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i], z = w[i];
if (dist[y] > dist[x] + z)
{
dist[y] = dist[x] + z;
if (!v[y])
{
q.push(y);
v[y] = 1;
}
}
}
}
}
// s1, s2, ..., sp即為環上點
void get_cycle(int x, int y, int i)
{
cnt++; // 環的數量+1
sum[y] = w[i];
br[i] = br[i ^ 1] = 1;
while (y != x)
{
in[y] = cnt;
int next_y = e[fa[y] ^ 1];
sum[next_y] = sum[y] + w[fa[y]];
br[fa[y]] = br[fa[y] ^ 1] = 1;
add(x, y, dist[y] - dist[x]);
add(y, x, dist[y] - dist[x]);
y = next_y;
}
in[x] = cnt;
len_cycle[cnt] = sum[x]; // 環總長度
}
void dfs(int x)
{
dfn[x] = ++num;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
fa[y] = i;
dfs(y);
}
else if ((i ^ 1) != fa[x] && dfn[y] >= dfn[x])
get_cycle(x, y, i);
}
}
void bfs()
{
d[1] = 1;
q.push(1);
while (!q.empty())
{
int x = q.front(); q.pop();
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!d[y] && !br[i])
{
q.push(y);
d[y] = d[x] + 1;
f[y][0] = x;
for (rint j = 1; j <= 20; j++)
f[y][j] = f[f[y][j - 1]][j - 1];
}
}
}
}
int calc(int x, int y)
{
if (d[x] < d[y]) swap(x, y);
int ox = x, oy = y;
for (rint i = 20; i >= 0; i--)
if (d[f[x][i]] >= d[y])
x = f[x][i];
if (x == y) return dist[ox] - dist[oy];
for (rint i = 20; i >= 0; i--)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
if (!in[x] || in[x] != in[y])
return dist[ox] + dist[oy] - 2 * dist[f[x][0]];
int l = abs(sum[y] - sum[x]); // 環上某個方向的距離
return dist[ox] - dist[x] + dist[oy] - dist[y] + min(l, len_cycle[in[x]] - l);
}
signed main()
{
cin >> n >> m >> t;
idx = 1;
for (rint i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
spfa();
dfs(1);
bfs();
for (rint i = 1; i <= t; i++)
{
int a, b;
cin >> a >> b;
cout << calc(a, b) << endl;
}
return 0;
}