解题报告-梦熊 CSP-S2025 模拟赛T2

P14309 【MX-S8-T2】配对

题目背景

争者留其名。

题目描述

给定一个 \(n\) 个点的树,点的编号为 \(1 \sim n\),边的编号为 \(1 \sim n - 1\)。第 \(i\) 条边连接 \(u_i\)\(v_i\),长度为 \(w_i\)。每个点有个 01 权值 \(c_i\)

现在你可以至多进行一次下面操作:选择两个点 \(u,v\),交换 \(c_u, c_v\) 的值。

\(\operatorname{dis}(u,v)\) 表示树上两点 \(u\)\(v\) 之间的距离,距离定义为连接它们的唯一简单路径中边的长度之和。

接下来依次进行下面的操作:

  1. 定义一个变量 \(r=0\)
  2. 选择两个不同的点 \(u, v\),使得 \(c_u=c_v=1\),若无法选出则结束。
  3. \(c_u=c_v=0\),令 \(r\) 加上 \(\operatorname{dis}(u,v)\)
  4. 回到第 2 步。

你希望通过选择合适的操作(包括初始时的交换 \(c_u, c_v\) 的操作,以及选择 \(u, v\)\(r\) 增加 \(\operatorname{dis}(u,v)\) 的操作)以最小化结束时的 \(r\),求出 \(r\) 可能的最小值。

输入格式

第一行,一个正整数 \(n\)

第二行,\(n\) 个非负整数 \(c_1, \ldots, c_n\),表示每个点的权值。

接下来 \(n-1\) 行,第 \(i\) 行三个正整数 \(u_i,v_i,w_i\),表示第 \(i\) 条边。

输出格式

输出一行,一个整数 \(r\)

输入输出样例 #1

输入 #1

8
1 0 0 0 1 1 1 1
1 2 4
1 3 3
2 4 2
2 5 1
3 6 1
3 7 2
4 8 1

输出 #1

4

说明/提示

【样例解释 #1】

一种可能的操作方式是:

首先一次操作选择 \(u=8, v=2\),交换 \(c_u,c_v\),目前 \(c\)\(1\) 的位置有 \(1,2,5,6,7\)

接下来,\(r=0\)

  1. 选择 \(u=2,v=5\)\(\operatorname{dis}(2,5)=1\)\(r\) 变为 \(1\)
  2. 选择 \(u=6,v=7\)\(\operatorname{dis}(6,7)=3\)\(r\) 变为 \(4\)
  3. 无法继续选出两个位置 \(u,v\) 满足 \(u\ne v\)\(c_u=c_v=1\),操作结束。

最终答案 \(r=4\),可以证明没有更小的答案。

【样例 #2】

见附件中的 \(\textbf{\textit{match/match2.in}}\)\(\textbf{\textit{match/match2.ans}}\)

该组样例满足测试点 \(3\sim 5\) 的约束条件。

【样例 #3】

见附件中的 \(\textbf{\textit{match/match3.in}}\)\(\textbf{\textit{match/match3.ans}}\)

该组样例满足测试点 \(6\sim 10\) 的约束条件。

【样例 #4】

见附件中的 \(\textbf{\textit{match/match4.in}}\)\(\textbf{\textit{match/match4.ans}}\)

该组样例满足测试点 \(15\sim 20\) 的约束条件。

【数据范围】

本题共 \(20\) 个测试点,每个 \(5\) 分。

对于所有数据,保证:

  • \(2 \leq n \leq 10^6\)
  • \(1 \leq w_i \leq 10^9\)
  • \(c_i \in \{0, 1\}\)
  • \(1 \leq u_i, v_i \leq n\)
  • 保证输入的边构成一棵树。
测试点编号 \(n \leq\) 特殊性质
\(1 \sim 2\) \(5\)
\(3 \sim 5\) \(100\) ^
\(6 \sim 10\) \(300\)
\(11 \sim 12\) \(10^4\) ^
\(13 \sim 14\) \(10^6\) ^
\(15 \sim 20\) ^
  • 特殊性质:保证 \(\sum_{i=1}^n c_i\) 为偶数。

解题报告

参考题解:题解:P14309 【MX-S8-T2】配对 - 洛谷专栏

哇啊啊啊啊!!!我的树形 DP 简直是一坨!

不过也确实是个很好的题。

简化问题,我们先不考虑交换操作。

首先,对于 \(\sum c_i\) 为偶数的情况,对于一颗以节点 \(u\) 为根的子树,只有子树内的点的 \(\sum c_i\) 为奇数时,我们才统计上从 \(u\) 的父节点到 \(u\) 的边的边权。

对于树上的任意一条边 \(e(x,y)\),可以把整棵树分成两个联通块点集 \(x,y\),由于 \(\sum c_i\) 为偶数,那么 \(\sum c_i(i \in x)\)\(\sum c_j(j \in y)\) 一定奇偶性相同。边 \(e(x,y)\) 产生的费用就是经过这条边的配对个数。

当两个联通块的 \(\sum c_i\) 同为奇数时,边 \(e(x,y)\) 肯定会被经过一次。

对于两个点 \(x_1\)\(x_2\) \(\in x\) 和另两个点 \(y_1、y_2\in y\),存在两种配对:\(x_1 \rightarrow u \rightarrow v \rightarrow y_1,x_2 \rightarrow u \rightarrow v \rightarrow y_2\)\(x_1 \rightarrow u \rightarrow x_2,y_1 \rightarrow v \rightarrow y_2\)

显然,这两种都有四段相同的路径 $ x_1 \rightarrow u,x_2 \rightarrow u,y_1 \rightarrow v,y_2 \rightarrow v$ ,但是第一种配对还多了两倍边 \(e(x,y)\) 的费用,不如第二种。

对于 \(\sum c_i\) 为奇数的情况,我们发现,使一个点不和其它点匹配,本质上就是把它的 \(c_i\) 变成 \(0\),可以考虑树形 DP,设 \(dp[u][0/1]\) 表示把子树 \(u\) 中把 \(0/1\)\(c_i=1\) 点变成 \(c_i=0\),方程很好推,暴力合并子节点的解在算上自己的价值就好了。

再考虑加上交换操作,等价为把其中一个 \(c_i=1\) 变成 \(c_i=0\),在把另一个 \(c_i=0\) 变成 \(c_i=1\),可以继续树形 DP。

最终的状态数组为 \(dp[u][0/1/2][0/1]\),表示把子树 \(u\)\(0/1/2\)\(c_i=1\) 点变成 \(c_i=0\),把 \(0/1\)\(c_i=0\) 的点变成 \(c_i=1\)

转移方程很像树上背包,具体看代码。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF=(1E+18)+10;
const int N=1000001;

#define ckmax(x,y) ( x=max(x,y) )
#define ckmin(x,y) ( x=min(x,y) )

inline int read()
{
	int f=1,x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch))  { x=(x<<3)+(x<<1)+ch-'0'; ch=getchar(); }
	return f*x;
}

bool c[N];
int n;

struct edge
{
    int next,to;
    int dis;
}e[N<<1];
int head[N],tot;

inline void add_edge(int u,int v,int w)
{
    e[++tot]=(edge){ head[u],v,w };head[u]=tot;
    e[++tot]=(edge){ head[v],u,w };head[v]=tot;
}

int cnt[N],w[N],d[N];

void dfs(int u,int fa)
{
    cnt[u]=c[u];
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa) continue;
        dfs(v,u);
        cnt[u]+=cnt[v];
        w[v]=e[i].dis;
    }
    cnt[u]%=2;
}

int dp[N][3][2];
int tmp[3][2];

void calc(int u,int fa)
{
    for(int i=0;i<3;i++)
     for(int j=0;j<2;j++)
        dp[u][i][j]=INF;
    if(d[u]==1 && u!=1)
    {
        dp[u][0][0]=w[u]*cnt[u];
        if(c[u])
          dp[u][1][0]=(!cnt[u])*w[u];
        else
          dp[u][0][1]=(!cnt[u])*w[u];
        return ;
    }
    dp[u][0][0]=0;
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa) continue;
        calc(v,u);
        memcpy(tmp,dp[u],sizeof(dp[u]));
        for(int j=0;j<3;j++)
         for(int k=0;k<2;k++)
         {
             dp[u][j][k]=INF;
             for(int s=0;s<=j;s++)
              for(int t=0;t<=k;t++)
                ckmin(dp[u][j][k],tmp[s][t]+dp[v][j-s][k-t]);
         }
    }

    if(c[u])
    {
        for(int i=2;i>0;i--)
         for(int j=0;j<2;j++)
           ckmin(dp[u][i][j],dp[u][i-1][j]);
    }
    else
    {
        for(int i=2;i>=0;i--)
          ckmin(dp[u][i][1],dp[u][i][0]);
    }
    for(int i=0;i<3;i++)
     for(int j=0;j<2;j++)
       dp[u][i][j]+=w[u]*( cnt[u]^( (i+j)%2 ) );
}

signed main()
{
	freopen("MX-S8-T2.in","r",stdin);
	freopen("MX-S8-T2.out","w",stdout);
    n=read();
    for(int i=1;i<=n;i++) c[i]=read();
    for(int i=1;i<n;i++)
    {
        int u=read(),v=read(),w=read();
        add_edge(u,v,w);
        d[u]++,d[v]++;
    }
    dfs(1,0),calc(1,0);
    if(!cnt[1])
      printf("%lld\n",min( dp[1][1][1],dp[1][0][0] ) );
    else
      printf("%lld\n",min( dp[1][2][1],dp[1][1][0] ) );
	return 0;
}
posted @ 2025-10-27 19:42  南北天球  阅读(15)  评论(0)    收藏  举报