JOISCD4T1

给定一棵树,每个节点有颜色,每次可以合并两个颜色 x,yx,y 使得所有颜色为 yy 的颜色都变成 xx。求最少的合并次数使得有一个颜色是联通的。
1n2000001\leq n\leq 200000

这题我考试的时候看错题自闭一整天

正经做法有两种:点分治和树剖优化建图跑SCC。

点分治

每次钦定这个分治中心的颜色必须选。而之前已经计算过的颜色不能再选了,因为那样不会更优。于是可能被选到的点就被限制在当前分治到的这个联通块中,于是就可以点分治做了。不断迭代地去找需要选的颜色,注意保证复杂度。

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=2e5+50;
int n,k,c[N],f[N],q[N],l,r,st[N],tp,zz[N];bool ban[N],be[N],fail,vvis[N];
vector<int>v[N],vv[N];
int sum,dat,rt,sz[N],ans;bool vis[N];
void findr(int x,int ff){
	int mx=0;sz[x]=1;
	for(int i=0,y;i<v[x].size();i++)
		if(!vis[y=v[x][i]]&&y!=ff)
			findr(y,x),sz[x]+=sz[y],mx=max(mx,sz[y]);
	mx=max(mx,sum-sz[x]);if(mx<dat)dat=mx,rt=x;
}
void dfs(int x,int ff,int z){
	f[x]=ff;if(!ban[c[x]])vv[c[x]].pb(x);
	if(!zz[c[x]])zz[c[x]]=z;else if(zz[c[x]]!=z)st[++tp]=c[x];
	for(int i=0,y;i<v[x].size();i++)
		if(!vis[y=v[x][i]]&&y!=ff)dfs(y,x,f[x]?z:y);
}
void dfs2(int x,int ff){
	vv[c[x]].clear(),vvis[x]=0;be[c[x]]=zz[c[x]]=0;
	for(int i=0,y;i<v[x].size();i++)
		if(!vis[y=v[x][i]]&&y!=ff)dfs2(y,x);
}
void solve(int x){
	vis[x]=1;
	dfs(x,0,0);be[q[l=r=1]=c[x]]=1;vvis[x]=1;fail=ban[c[x]];
	while(l<=r){
		int x=q[l++];
		for(int i=0;i<vv[x].size();i++){
			for(int j=vv[x][i];!vvis[j];j=f[j]){
				if(ban[c[j]]){fail=1;break;}vvis[j]=1;
				if(!be[c[j]])be[q[++r]=c[j]]=1;
			}
			if(fail)break;
		}
		if(fail)break;
	}
	if(!fail)ans=min(ans,r);
	while(tp)ban[st[tp--]]=1;ban[c[x]]=1;
	dfs2(x,0);
	for(int i=0,y;i<v[x].size();i++)if(!vis[y=v[x][i]])
		sum=sz[y],dat=1e9,findr(y,x),solve(rt);
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1,x,y;i<n;i++)
		scanf("%d%d",&x,&y),
		v[x].pb(y),v[y].pb(x);
	for(int i=1;i<=n;i++)scanf("%d",&c[i]);
	ans=sum=n;dat=1e9;findr(1,0);solve(rt);
	cout<<ans-1;
}

树剖

考虑如果两个颜色 x,yx,y 有选了颜色 xx 就必须选颜色 yy 的关系(yyxx 的路径上),我们就从 xxyy 连边。这样建好图之后如果要选 xx 就要把所有它能到的点都选上。这其实是一个很经典的模型:显然一个SCC中的点要选会一块选,而一定是选出度为 00 的SCC比较优。于是建好图跑tarjan就好了。

发现连边其实也是比较显然的。设颜色 xx 的所有点的lca为 zz,那么颜色 xx 会向 zz 与所有颜色为 xx 的点的路径上的颜色连边。连树上一条路径的话可以想到树剖优化,但是裸的树剖套线段树是 nlog2nnlog^2n 的,可能过不去。我们只要利用剖出的路径会是一些重链的前缀这个性质,预处理重链前缀的节点就可以做到 nlognnlogn

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=2e5+50,M=6e6+50;
int n,k,f[N],sz[N],d[N],dfn[M],idfn[N],tp[N],clk,rt[N],c[N],ans;
int lc[M],rc[M],rr,cnt,id[N],low[M],st[M],tt,scc,val[M],bel[M];bool ins[M],ban[M];
vector<int>v[N],vv[M],mem[M];
void dfs1(int x){
    if(f[x])v[x].erase(find(v[x].begin(),v[x].end(),f[x]));
    d[x]=d[f[x]]+1;sz[x]=1;
    for(int i=0,y;i<v[x].size();i++){
        f[y=v[x][i]]=x,dfs1(y),sz[x]+=sz[y];
        if(sz[y]>sz[v[x][0]])swap(v[x][i],v[x][0]);
    }
}
void dfs2(int x){
    tp[x]^x?vv[id[x]=++cnt].pb(id[f[x]]),vv[cnt].pb(c[x]),0:id[x]=c[x];
    idfn[dfn[x]=++clk]=x;
    for(int i=0,y;i<v[x].size();i++)
        y=v[x][i],tp[y]=i?y:tp[x],dfs2(y);
}
int lca(int x,int y){
    for(;tp[x]!=tp[y];x=f[tp[x]])
        if(d[tp[x]]<d[tp[y]])swap(x,y);
    return d[x]<d[y]?x:y;
}
void build(int &x,int l,int r){
    if(l==r){x=c[idfn[l]];return;}
    int mid=(l+r)>>1;x=++cnt;
    build(lc[x],l,mid);build(rc[x],mid+1,r);
    vv[x].pb(lc[x]);vv[x].pb(rc[x]);
}
void query(int x,int l,int r,int ql,int qr,int d){
    if(l>qr||r<ql)return;
    if(l>=ql&&r<=qr){vv[d].pb(x);return;}
    int mid=(l+r)>>1;
    query(lc[x],l,mid,ql,qr,d);
    query(rc[x],mid+1,r,ql,qr,d);
}
void query(int x,int y,int d){
    for(;tp[x]!=tp[y];y=f[tp[y]])vv[d].pb(id[y]);
    query(rr,1,n,dfn[x],dfn[y],d);
}
void cmin(int &x,int y){y<x?x=y:0;}
void tarjan(int x){
    dfn[x]=low[x]=++clk;
    st[++tt]=x;ins[x]=1;
    for(int i=0,y;i<vv[x].size();i++)
        if(!dfn[y=vv[x][i]])tarjan(y),cmin(low[x],low[y]);
        else if(ins[y])cmin(low[x],dfn[y]);
    if(low[x]==dfn[x]){
        int y;++scc;
        do{
            y=st[tt--];ins[y]=0;
            val[scc]+=y<=k;bel[y]=scc;
            mem[scc].pb(y);
        }while(y!=x);
    }
}
int main(){
    scanf("%d%d",&n,&k);cnt=ans=k;
    for(int i=1,x,y;i<n;i++)
        scanf("%d%d",&x,&y),
        v[x].pb(y),v[y].pb(x);
    for(int i=1;i<=n;i++)scanf("%d",&c[i]);
    dfs1(1);dfs2(tp[1]=1);build(rr,1,n);
    for(int i=1;i<=n;i++){int &z=rt[c[i]];z=z?lca(z,i):i;}
    for(int i=1;i<=n;i++)query(rt[c[i]],i,c[i]);
    memset(dfn,0,sizeof(dfn));clk=0;
    for(int i=1;i<=cnt;i++)if(!dfn[i])tarjan(i);
    for(int i=1;i<=scc;i++){
        for(int j=0;j<mem[i].size();j++){
            for(int x=mem[i][j],y,k=0;k<vv[x].size();k++){
                if(bel[y=vv[x][k]]!=i&&(val[bel[y]]||ban[bel[y]]))ban[i]=1;
            }
        }
        if(!ban[i]&&val[i])cmin(ans,val[i]);
    }
    cout<<ans-1;
    return 0;
}

正题

在比赛后当天与四川神仙讨论时,他说他就写了个乱搞:倍增lca和tarjan。
当时我就觉得不太会啊,这又是另一种神仙做法吧。
后来讲了题,发现哪种也不是他的,又去问了问他。
他的做法:

对于每个节点,如果它不是所在颜色的lca,那么就把它的颜色向它父亲的颜色连边,然后就跑tarjan找到出度为 00 的SCC。

很困难地理解了他的意思之后提出了对正确性的质疑,并举了例子。他说:你说的有道理。但这不影响正确性啊。
我看神仙已经有对我烦的意思了,于是也没再多打扰他。

玩了好几个数据发现都没有问题。后来实现了他的做法(优化了一下实现了 O(n)O(n)),轻松地拿到了UOJ,LOJ的最短代码和最快代码!发到了群里炫耀,并求助大家正确性的证明。
z7z刚开始很确信这个算法是错误的,并尝试给出hack。他说hack成功了,但其实还是假了。
后来z7z也相信这个算法是正确的了,并给出了感性理解(谁不会感性理解啊)。

我也开始好好思考怎么证明它。我有了一个证明的思路,但是发现有一个地方说不过去。然后就想构造一个这样的数据说不定就有证明的思路呢??

然后。。这个算法它死了23333333谁也没想到是这样的结局233333。

于是,忍痛亲手hack自己的最优解.jpg
最后放出hack数据结束这篇扯淡23333

7 3
1 2
2 3
3 4
4 5
5 6
6 7
3
1
3
2
1
2
1

错代码还是不要放了,虽然它看起来真的很优秀QAQ