zoukankan      html  css  js  c++  java
  • AtCoder apc001_f

    洛谷题目页面传送门 & AtCoder题目页面传送门

    给定一棵带边权的树(T=(V,E),|V|=n,|E|=n-1),每次操作可以选择任意一条链和任意一个整数(x),使这条链上所有边边权都异或上(x)。问最少用多少次操作使得所有边的边权都变为(0)

    (ninleft[2,10^5 ight],forall (x,y,v)in E,vin[0,15])

    考虑到链异或很不好想,不妨用差分把它转化为双点操作。

    先介绍一下树上前缀和和树上差分:树上某点处的前缀和是以它为根的子树和。由于前缀和和差分是互逆运算,很容易推出树上某点处的差分是它减去它所有儿子。设差分数组为(d),那么对某一条祖先(x)到晚辈(y)的链上所有点增加(v)可以转化为令(d_y=d_y+v,d_{fa_x}=d_{fa_x}-v)

    本题权值在边上,我们可以用每个点(除了根)代表它连向父亲的那条边。并且本题的操作是异或,异或的逆运算仍然是异或。设差分数组为(d),那么对某一条祖先(x)到晚辈(y)的链上所有边异或(v)可以转化为令(d_y=d_yoplus v,d_x=d_xoplus v)。对于要异或的某条链(x o y),显然可以拆成(x omathrm{LCA}(x,y),y omathrm{LCA}(x,y)),在(mathrm{LCA}(x,y))处差分异或的(2)次抵消了,所以任意一条链异或依然可以转化为令(d_x=d_xoplus v,d_y=d_yoplus v)。最终的目标将所有边的边权都变成(0)等价于将差分数组清零。于是只需要求出(T)的差分数组(显然不包括根),问题就转化成了:给定(n-1)个数,每次可以将其中(1sim2)个数异或上同一个数,求清零的最小次数。

    首先,所有的(0)不用管,当作不存在。显然存在一种方案:将每个数异或上自己,这样(n-1)次操作即可清零。这种方案是每次清空(1)个数的。考虑能不能用(<x)次清空(x)个数来优化方案。不难想到,唯一的优化方式是:若(x)个数的异或和为(0),那么可以用(x-1)次操作将它们清零。

    显然,若存在(2)个数相等,那么它们满足异或和为(0)的条件,于是我们贪心地将它们(1)次清零。这样贪心看起来很正确,因为显然用(x-1)次操作清零的集合越多越优,于是集合们的大小要尽可能小。然鹅实际上这样贪心的确是对的。证明:若存在(2)个相同的数,它们没有被(1)次清零,分(4)种情况:

    1. 它们都异或上自己来清零,操作数为(2)。那么显然将它们(1)次清零更优;
    2. 它们其中一个异或上自己来清零,另一个包含在一个(x-1)次操作清零的集合里。设这个集合大小为(s),那么操作数为(1+s-1=s)。若将这(2)个数(1)次清零,其他(s-1)个数都异或上自己来清零,操作数仍然是(1+s-1=s),没有变差;
    3. 它们包含在不同的(x-1)次操作清零的集合里。设这(2)个集合大小分别为(s_1,s_2),那么操作数为(s_1-1+s_2-1=s_1+s_2-2)。若将这(2)个数(1)次清零,那么剩下来的(s_1+s_2-2)个数异或和显然为(0),可以(s_1+s_2-3)次清零,操作数仍然是(1+s_1+s_2-3=s_1+s_2-2),没有变差;
    4. 它们包含在同一个(x-1)次操作清零的集合里。设这个集合大小为(s),那么操作数为(s-1)。若将这(2)个数(1)次清零,其他(s-2)个数都异或上自己来清零,操作数仍然是(1+s-2=s-1),没有变差。

    综上,对于任意一个方案,若存在(2)个相同的数,它们没有被(1)次清零,那么若将它们(1)次清零,有办法使得方案不会变差。所以,必存在一种最优方案,其中尽可能将所有相同的(2)个数(1)次清零。得证。

    我们用桶来存储这(n-1)个数,设(buc_i)表示数(i)的个数。那么按照上面贪心的方案,尽可能将所有相同的(2)个数(1)次清零需要(sumlimits_{i=1}^{15}leftlfloordfrac{buc_i}2 ight floor)次操作,数(i)会剩下(imod2in{0,1})个。对于剩下的这么少的数,我们就可以状压DP了。设(dp_i)表示剩下(1)(x)当且仅当(xin i)时清空这(|i|)个数需要的最小操作数。边界为(dp_{varnothing}=0),目标为(sumlimits_{i=1}^{15}leftlfloordfrac{buc_i}2 ight floor+dp_{{xmid buc_xmod2=1}}),状态转移方程为(dp_i=minegin{cases}|i|\|i|-1&igopluslimits_{jin i}j=0\minlimits_{j eqvarnothing,jsubsetneq i}{dp_j+dp_{i-j}}end{cases})(很简单吧)。时间复杂度为一个常数(3^{15})(枚举子集)。还要再加上一些树上操作的(mathrm O(n))

    下面是AC代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define mp make_pair
    #define X first
    #define Y second
    #define pb push_back
    const int inf=0x3f3f3f3f;
    int ppc(int x){return __builtin_popcount(x);}
    const int N=100000;
    int n;//树的大小 
    vector<pair<int,int> > nei[N+1];//邻接表 
    int buc[16];//桶 
    int dfs(int x=1,int fa=0){//算差分数组&装进桶里,返回x的儿子的异或和 
    	int xsm=0;//儿子的异或和 
    	for(int i=0;i<nei[x].size();i++){
    		int y=nei[x][i].X,v=nei[x][i].Y;
    		if(y==fa)continue;
    		xsm^=v;//算儿子的异或和 
    		buc[dfs(y,x)^v]++;//将儿子y处的差分装进桶里 
    	}
    	return xsm;
    }
    int dp[1<<15];
    int main(){
    	cin>>n;
    	for(int i=1;i<n;i++){
    		int x,y,z;
    		cin>>x>>y>>z;
    		x++;y++;
    		nei[x].pb(mp(y,z));nei[y].pb(mp(x,z));
    	}
    	dfs();
    	for(int i=1;i<1<<15;i++){//DP 
    		dp[i]=ppc(i);//状态转移方程min中第1个式子 
    		int xsm=0;
    		for(int j=1;j<=15;j++)if(i&1<<j-1)xsm^=j;//集合内的数的异或和 
    		if(!xsm)dp[i]=ppc(i)-1;//状态转移方程min中第2个式子 
    		for(int j=i-1&i;j;j=j-1&i)dp[i]=min(dp[i],dp[j]+dp[i^j]);//状态转移方程min中第3个式子  
    	}
    	int ans=0,msk=0;
    	for(int i=1;i<=15;i++)ans+=buc[i]>>1,msk|=(buc[i]&1)<<i-1;
    	cout<<ans+dp[msk];//目标 
    	return 0;
    }
    
  • 相关阅读:
    关于宇宙大爆炸的理论模型
    算法系列2《RSA》
    Codeforces Round #248 (Div. 1)——Nanami&#39;s Digital Board
    Cocos2d-x场景变化相关功能介绍
    NYOJ 745 蚂蚁问题(两)
    quick-cocos2d-x endToLua 退出会卡住
    编程算法
    linux基础知识1
    URAL 1553. Caves and Tunnels 树链拆分
    2014/11/13_ 随想
  • 原文地址:https://www.cnblogs.com/ycx-akioi/p/AtCoder-apc001-f.html
Copyright © 2011-2022 走看看