P14309 【MX-S8-T2】配对

题目

题意

树上有一些标记为 \(1\) 和 \(0\) 的点(下文我们将其分别称作黑点和白点)。

每次必须选两个黑点配对,代价是它们之间的树上距离。一个点配对后就不能再使用。

在开始之前,还可以交换两个点的权值。就是相当于:把一个黑点移动到一个原本是白点的位置,然后也可以选择不交换。

目标是让所有配对的距离之和最小。

思路

首先我们肯定不能直接枚举所有配对,会超时。

所以我们不看配对,我们把注意力转移到边上来。

假设树上有一条边,把它切断后,整棵树会分成左右两部分。设这条边权值为 \(w\),其中一侧子树内有 \(k\) 个黑点。

那么思考,最终配对的总距离中,这条边至少会被经过多少次?显然:

  • 如果 \(k\) 是偶数,这一侧的黑点可以两两配对,不需要有配对路径经过这条边。
  • 如果 \(k\) 是奇数,则至少有一个黑点必须和另一侧的黑点配对,因此这条边至少要被经过一次。

也就是说,每条边只在必要时经过一次,不必要时一次也不经过。

所以,在黑点总数为偶数且不进行交换时,答案为:\(\sum_{u\ne root}^{} w_{u}(cnt_{u}\bmod 2 )\)。其中,根可任选,\(w_{u}\) 表示节点 \(u\) 与父节点之间的边权,\(cnt_{u}\) 表示 \(u\) 的子树中黑点的数量。

也就是说:一条边的贡献只取决于它下面的黑点数量是奇数还是偶数。

这样一来,我们不再需要真的去安排每一对黑点,只需要统计每个子树的奇偶性了。

接下来,我们考虑一次交换改变了什么?

假设原来有一个黑点在 \(x\),一个白点在 \(y\),交换后:

  • \(x\):黑变白;
  • \(y\):白变黑。

对于任意一条边,只有当这条边位于 \(x\) 到 \(y\) 的路径上时,它下面的黑点数量奇偶性才会改变。

不过问题来了,我们并不知道 \(x\) 和 \(y\) 是谁,但是如果枚举的话肯定也会超时。那咋整?

稍加思索我们可以想到,应该把“交换了哪些点”转化为树形 \(DP\) 的状态。

对于交换情况我们可以用四种状态表示:对于节点 \(u\):

状态 含义
0 表示子树权值没有被修改
1 子树里一个原本为 \(1\) 的点变成 \(0\)
2 子树里一个原本为 \(0\) 的点变成 \(1\)
3 子树里完成了一次交换

这里注意,状态 \(1\) 和状态 \(2\) 可以单独出现在某棵子树中,因为交换的两个点可能分别位于不同的子树。只有合并整棵树时,才能要求交换完整。

接一来,还要处理黑点总数为奇数的情况,因为如果黑点总数为奇数,最终会剩下一个黑点无法配对。

这时,我们可以再加一维状态,表示是否有一个原本为 \(1\) 的点被排除在配对之外:

  • \(j=0\):没有排除原本为 \(1\) 的点;
  • \(j=1\):排除了一个原本为 \(1\) 的点。

于是乎,我们定义:\(dp_{u,s,j}\),表示在节点 \(u\) 的子树内,交换状态为 \(s\)、排除状态为 \(j\) 时,能够得到的最小费用。总共只有 \(2\times 4=8\) 个状态。

这里的费用不只是子树内部的配对费用,还包含子树根节点与父节点之间的边的贡献。这样合并子树时,就能直接累加。

最后一步,怎样计算每条边的贡献?我们先不考虑排除黑点的情况。

记

\(p_{u}=(\sum_{v\in subtree(u)}^{} c_{v})\bmod 2\)

即原始子树黑点数的奇偶性。如果没有修改,这条父边的贡献就是:\(w_{u}p_{u}\)。

如果子树里恰好发生了一次黑白变化,奇偶性就翻转,贡献就变成:\(w_{u}(p_{u}\oplus 1)\)。(\(\oplus\) 表示异或)

当然,如果已经排除了一个黑点,奇偶性还要再翻转一次。因此,边的贡献完全可以通过原始奇偶性和 \(DP\) 状态算出,不需要真的修改整棵树。

再再再接下来,那合并两个子树时怎么办?

假设当前正在处理节点 \(u\),已经合并了一部分儿子,现在准备合并儿子 \(v\)。

那么我们可以枚举:

  1. 已经处理部分的状态;
  2. 子树 \(v\) 的状态;
  3. 排除的黑点来自哪一边;
  4. 两边合并后属于哪个状态。

对于每种合法组合,把两部分费用加起来,再加上父边贡献,取最小值。

例如,不考虑排除黑点时:

  • 状态 \(0+0\rightarrow 0\);
  • 状态 \(1+0\rightarrow 1\);
  • 状态 \(0+2\rightarrow 2\);
  • 状态 \(1+2\rightarrow 3\);

(这就是树形 \(DP\) 中非常常见的子树合并。与普通的背包 \(DP\) 类似,只不过这里合并的是树的儿子,而不是物品。)

OK呀,以上是我的全部证词,啊bushi主要思路,其他的见代码吧。


代码 $ing$
/*@author:decentz*/
#include<bits/stdc++.h>
#define FRE(x,y) freopen(#x".in","r",stdin);freopen(#y".out","w",stdout);
#define LK_TIME cerr<<(1.0*clock()/CLOCKS_PER_SEC)<<'\n';
#define REP(i,a,b) for(int i=(a),i##end=(b);i<=i##end;i++)
#define PRE(i,a,b) for(int i=(a),i##end=(b);i>=i##end;i--)
#define MMS(arr,n) memset(arr,n,sizeof(arr));
#define DEBUG(x) cerr<<#x<<": "<<x<<'\n';
using namespace std;
using LL=long long;
const LL INF=(1LL<<60),_INF=LLONG_MIN;
bool STWZZ;
const int N=1e6+5;
int n,hd[N],ver[2*N],nxt[2*N],tot,c[N],fa[N],ord[N],cnt1;//ord建树时记录的遍历顺序;cnt1整棵树原始1的总数
LL w[2*N],W[N],dp[N][4][2],dt[4][2],p[N];//W是节点u到父节点的边权;dt是合并当前儿子之前,已经处理部分的DP值;p是u的子树内原始1的数量的奇偶性
void add(int u ,int v,int ww){
	ver[++tot]=v;nxt[tot]=hd[u];w[tot]=ww;hd[u]=tot;
	ver[++tot]=u;nxt[tot]=hd[v];w[tot]=ww;hd[v]=tot;
}
void build(){
	int tp=1;
	ord[1]=1;
	for(int i=1;i<=tp;i++){
		int u=ord[i];
		for(int e=hd[u];e;e=nxt[e]){
			int v=ver[e];
			if(v==fa[u]||v==1) continue;
			fa[v]=u;
			W[v]=w[e];
			ord[++tp]=v;
		}
	}
}
void init(){
	PRE(i,n,1){
		int u=ord[i];
		dp[u][0][0]=W[u]*p[u];//不修改,也不舍弃黑点
		if(c[u]){//u原本是1
			dp[u][2][0]=W[u]*(p[u]^1);//可以把u变成0,作为交换的第一半
			dp[u][0][1]=W[u]*(p[u]^1);//或者舍弃u这个原本的1
		}else{//u原本是0
			dp[u][1][0]=W[u]*(p[u]^1);//可以把u变成1,作为交换的另一半
			dp[u][1][1]=W[u]*p[u];//也可以让一个原本的1在其他位置被舍弃
		}
		for(int e=hd[u];e;e=nxt[e]){//合并
			int v=ver[e];
			if(fa[v]!=u) continue;
			REP(j,0,3){//保存合并前的状态
				REP(k,0,1){
					dt[j][k]=dp[u][j][k];
					dp[u][j][k]=INF;
				}
			}
			dp[u][0][0]=dt[0][0]+dp[v][0][0];//已处理部分没有修改;儿子子树也没有修改;合并后仍然没有修改。因为没有任何变化,所以边的贡献不需要修正。
			dp[u][1][0]=min(dt[1][0]+dp[v][0][0],dt[0][0]+dp[v][1][0]+W[u]*((p[u]^1)-p[u]));//两种可能:修改发生在已经处理的部分,儿子不修改;已处理部分不修改,修改发生在儿子子树。
			dp[u][2][0]=min(dt[2][0]+dp[v][0][0],dt[0][0]+dp[v][2][0]+W[u]*((p[u]^1)-p[u]));
			dp[u][3][0]=min(min(dt[3][0]+dp[v][0][0],dt[0][0]+dp[v][3][0]),min(dt[1][0]+W[u]*(p[u]-(p[u]^1))+dp[v][2][0],dt[2][0]+W[u]*(p[u]-(p[u]^1))+dp[v][1][0]));//已处理部分已经完成交换,儿子不修改;已处理部分不修改,儿子已经完成交换;已处理部分发生状态 1,儿子发生状态 2;已处理部分发生状态 2,儿子发生状态 1。
			REP(j,0,1){// 舍弃一个黑点:枚举舍弃发生在哪一部分
				dp[u][0][1]=min(dp[u][0][1],dt[0][j]+W[u]*((p[u]^1)-(p[u]^j))+dp[v][0][1-j]);//状态0:p[u]^j表示旧的已处理部分在当前舍弃状态下的奇偶性;p[u]^1表示合并后总共舍弃一个1时的目标奇偶性;两者相减,就是这条父边需要调整的贡献。状态1、2、3的转移同理,只是要根据是否在已处理部分或儿子子树中发生了交换,决定父边奇偶性是否额外翻转。
				dp[u][1][1]=min(dp[u][1][1], min(dt[1][j]+W[u]*(p[u]-(p[u]^1^j))+dp[v][0][1-j],dt[0][j]+W[u]*(p[u]-(p[u]^j))+dp[v][1][1-j]));//状态1
				dp[u][2][1]=min(dp[u][2][1],min(dt[2][j]+W[u]*(p[u]-(p[u]^1^j))+dp[v][0][1-j],dt[0][j]+W[u]*(p[u]-(p[u]^j))+dp[v][2][1-j]));//状态2				
				dp[u][3][1]=min(dp[u][3][1],min(min(dt[3][j]+W[u]*((p[u]^1)-(p[u]^j))+dp[v][0][1-j],dt[0][j]+W[u]*((p[u]^1)-(p[u]^j))+dp[v][3][1-j]),min(dt[1][j]+W[u]*((p[u]^1)-(p[u]^j^1))+dp[v][2][1-j],dt[2][j]+W[u]*((p[u]^1)-(p[u]^j^1))+dp[v][1][1-j])));//状态3:完整交换,并舍弃一个黑点
			}
		}
	}
}
bool EDWZZ;
int main(){
	//FRE(,);
	//cerr<<(&EDWZZ-&STWZZ)/1024.0/1024.0<<'\n';
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n;
	REP(i,1,n){
		cin>>c[i];
		cnt1+=c[i];
		REP(j,0,3){
			REP(k,0,1){
				dp[i][j][k]=INF;
			}
		}
	}
	REP(i,2,n){
		int u,v,ww;cin>>u>>v>>ww;
		add(u,v,ww);
	}
	build();
	PRE(i,n,1){//逆序遍历,让儿子的结果先传给父亲。
		int u=ord[i];
		p[u]+=c[u];
		p[u]&=1;//只保留最低位
		if(fa[u]) p[fa[u]]^=p[u];
	}
	init();
	cout<<min(dp[1][0][cnt1&1],dp[1][3][cnt1&1]);//最终只能不交换或完整交换;cnt1&1:如果原始1的数量是偶数,那么不需要额外舍弃1,选择j=0;如果原始1的数量是奇数,那么必须舍弃一个原本的1,选择j=1。
	return 0;
}
//时间复杂度:O(n)

小 $tips$

遇到此类树上优化问题,可以按照此顺序思考:

  1. 不要一上来就写 \(DP\),先问能不能把答案拆到每条边上。树上路径问题往往可以从边被经过多少次来考虑。
  2. 再问边的贡献取决于什么。是子树大小、点权之和、奇偶性,还是某种最值?
  3. 把修改操作转化为有限状态。一次交换不意味着必须枚举两个点;尝试记录子树内发生了哪些变化。
  4. 设计状态时,确保子树之间的信息足够合并。比如这里用 \(4\) 种修改状态和 \(2\) 种奇偶状态。
  5. 最后检查复杂度和实现细节。尤其像本题 \(n=10^6\) 的范围。

posted @ 2026-10-21 19:44  decentz  阅读(8)  评论(1)    收藏  举报