比赛链接:link
C 边权转点权
题目大意是一棵树给定边权,都是非负数,树上的每条路径可以使得经过的边的边权减 1,问最少需要几次路径? 其中还有 q 次修改,每次修改一条边权,每次修改完之后输出更新后的答案。
经过分析可以发现,若记
w
(
u
,
v
)
w(u, v)
w(u,v) 为
u
u
u,
v
v
v 间的边权,那么就存在
w
(
u
,
v
)
w(u, v)
w(u,v) 条路径通过
u
,
v
u, v
u,v。可以举几个例子看看,比如
u
u
u 连有三个点
v
1
,
v
2
,
v
3
v_1, v_2, v_3
v1,v2,v3:
①若边权分别为 10,5,2, 可以用 7 条
u
v
1
uv_1
uv1 的路径可以与所有
u
v
2
,
u
v
3
uv_2, uv_3
uv2,uv3 的路径相连接,那么
u
u
u 至少是 3 条路径的端点;
②若边权分别是5,4,3,那么 3条
u
v
1
uv_1
uv1 的路径可以与 3条
u
v
2
uv_2
uv2 路径连接, 2条
u
v
1
uv_1
uv1 的路径可以与 2条
u
v
3
uv_3
uv3 路径连接,1条
u
v
2
uv_2
uv2 的路径可以与 1条
u
v
3
uv_3
uv3 路径连接,
u
u
u 端点数可以为
0
0
0。
②若边权分别是5,4,2,由于是奇数,那么不管怎么连接,
u
u
u 端点数至少为
1
1
1。
通过上面的举例我们可以得到,若是一个点的边权和为 d, 最大边权为 mx,大于其他边权和 d - mx, 那么这个点的路径端点数为 mx - (d - mx) = 2*mx - d;若最大边权小于等于其他边权,若 d 为偶数,则可以互相连接,端点数为 0, 否则奇数的话端点数为1。
于是我们需要维护每个点的边权和,这个用一个数组就可以,还需要维护最大边权,这个可以用 multiset 来做到;计算完所有点的端点数之后,除以 2 为我们所需要答案。每次更新我们只需要相应的更改数组和 multiset, 复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)。
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int, int> P;
const int maxn = 1e5 + 10;
const int INF = 0x3f3f3f3f;
const ll mod = 998244353;
int x[maxn], y[maxn], val[maxn], n, q, p, w;
ll d[maxn];
multiset<int> s[maxn];
int cal(int x) //计算每个点的路径端点数
{
ll mx = *s[x].rbegin();
if(mx * 2 <= d[x]) return d[x] & 1;
return 2 * mx - d[x];
}
int main()
{
scanf("%d %d", &n, &q);
for(int i = 1; i < n; i++)
{
scanf("%d %d %d", &x[i], &y[i], &val[i]);
d[x[i]] += val[i]; d[y[i]] += val[i];
s[x[i]].insert(val[i]); s[y[i]].insert(val[i]);
}
ll ans = 0;
for(int i = 1; i <= n; i++) ans += cal(i);
printf("%lld\n", ans / 2);
while(q--)
{
scanf("%d %d", &p, &w);
ans -= cal(x[p]) + cal(y[p]);
s[x[p]].erase(s[x[p]].find(val[p])); s[y[p]].erase(s[y[p]].find(val[p]));
s[x[p]].insert(w); s[y[p]].insert(w);
d[x[p]] += w - val[p]; d[y[p]] += w - val[p];
val[p] = w;
ans += cal(x[p]) + cal(y[p]);
printf("%lld\n", ans / 2);
}
}
J 树型dp & 二分图权值匹配
题目大意是给定两棵形状一样的树,每个点都有不同的值,问第一棵树最少改变几个结点的值可以和第二棵完全相同?
我一开始想的是递归去做,首先判断某两个点的子树是否相同,然后对其相同的儿子分别去试试(比如儿子
1
,
2
,
3
1, 2, 3
1,2,3形状相同,那么可以有
3
!
3!
3!种试法),这样搞是阶乘复杂度,炸内存了。。
其实这个匹配的过程可以用二分图来完成,降到多项式复杂度。首先
d
p
[
x
]
[
y
]
dp[x][y]
dp[x][y] 表示第一二棵树分别以
x
x
x,
y
y
y 为结点的子树要相同的最少花费,若是形状都不一样就让值为
I
N
F
INF
INF,这个过程我们可以先进行树型dp,求出
d
p
[
a
]
[
b
]
dp[a][b]
dp[a][b] (
a
a
a 和
b
b
b 分别为
x
x
x,
y
y
y 的儿子),然后进行二分图的权值匹配,得到
d
p
[
x
]
[
y
]
dp[x][y]
dp[x][y],这个思想比较巧妙。
然后顺便复习了一下 km 算法的板子,才知道 km 算法是完备匹配…由于km 算法的数组 val 是全局变量,要首先将所有的
d
p
[
a
]
[
b
]
dp[a][b]
dp[a][b]算完,再去更新 km 算法的数组,切记。
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int, int> P;
const int maxn = 510;
const int INF = 1e6;
const ll mod = 998244353;
int val[maxn][maxn], vis_A[maxn], vis_B[maxn], match[maxn]; //vis来记录已被匹配的,match记录B方匹配到了A方的哪个
int ex_A[maxn], ex_B[maxn], slack[maxn], m, n; //记录A方和B方的期望, A方有m个,B方有n个
//slack 任意一个参与匹配A方能换到任意一个这轮没有被选择过的B方所需要降低的最小值
//这是二分图的最优匹配(首先是A集合的完备匹配,然后保证权值最大)
//所以一定保证 m <= n, 否则会陷入死循环,若是A集合点多的话可以把B集合补充到和A一样多,设置-INF的边
bool dfs(int x)
{
vis_A[x] = 1;
for(int i = 1; i <= n; i++)
{
if(!vis_B[i]) //每一轮匹配,B方每一个点只匹配一次
{
int gap = ex_A[x] + ex_B[i] - val[x][i];
if(gap == 0) //如果符合要求
{
vis_B[i] = 1;
if(!match[i] || dfs(match[i])) //如果v尚未匹配或者匹配了可以被挪走
{
match[i] = x;
return true;
}
}
else slack[i] = min(slack[i], gap);
}
}
return false;
}
int km()
{
memset(match, 0, sizeof(match)); //match为0表示还没有匹配
fill(ex_B + 1, ex_B + 1 + n, 0); //B方一开始期望初始化为0
for(int i = 1; i <= m; i++) //A方期望取最大值
{
ex_A[i] = val[i][1];
for(int j = 2; j <= n; j++)
ex_A[i] = max(ex_A[i], val[i][j]);
}
for(int i = 1; i <= m; i++) //尝试解决A方的每一个节点
{
memset(slack + 1, INF, sizeof(slack[0]) * n);
for(;;)
{
memset(vis_A + 1, 0, sizeof(vis_A[0]) * m); //记录AB双方有无被匹配过
memset(vis_B + 1, 0, sizeof(vis_B[0]) * n);
if(dfs(i)) break;
int d = INF;
for(int j = 1; j <= n; j++) if(!vis_B[j]) d = min(d, slack[j]);
//if(d == INF) break; //找不到完全匹配
for(int j = 1; j <= m; j++) if(vis_A[j]) ex_A[j] -= d;
for(int j = 1; j <= n; j++)
{
if(vis_B[j]) ex_B[j] += d;
else slack[j] -= d;
}
}
}
int ans = 0;
for(int i = 1; i <= n; i++)
{
if(match[i]) // 可以加 && val[match[i]][i] > -INF 去除一些匹配
ans += val[match[i]][i];
}
return ans;
}
vector<int> G1[maxn], G2[maxn];
int dp[maxn][maxn], rt1, rt2;
void solve(int x, int y)
{
if(G1[x].size() != G2[y].size())
{
dp[x][y] = INF;
return;
}
if(x != y) dp[x][y]++;
if(G1[x].size() == 0) return;
for(unsigned int i = 0; i < G1[x].size(); i++)
for(unsigned int j = 0; j < G2[y].size(); j++)
solve(G1[x][i], G2[y][j]); //切记先让子树完成之后再更新val
for(unsigned int i = 0; i < G1[x].size(); i++)
for(unsigned int j = 0; j < G2[y].size(); j++)
val[i+1][j+1] = -dp[G1[x][i]][G2[y][j]]; //因为是求最小匹配,所以置为负数,不存在的边置为-INF
m = n = G1[x].size();
int tmp = -km();
if(tmp >= INF) dp[x][y] = INF;
else dp[x][y] += tmp;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
int tmp;
scanf("%d", &tmp);
if(tmp == 0) rt1 = i;
else G1[tmp].pb(i);
}
for(int i = 1; i <= n; i++)
{
int tmp;
scanf("%d", &tmp);
if(tmp == 0) rt2 = i;
else G2[tmp].pb(i);
}
solve(rt1, rt2);
printf("%d\n", dp[rt1][rt2]);
}