2024.3.16 笔记
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;
}