[Hnoi2010]City 城市建设 CDQ分治

[Hnoi2010]City 城市建设

Part 0

怎么也想不到正解会是CDQ分治,很妙的一道题。

CDQ分治就是将在线的问题转化为离线问题,将变化的数据变为不变的数据,缩小问题规模。

着眼于每个询问间的重复的操作,让一些操作不重复执行多遍。

同时还要保证分治的子区间执行的操作的复杂度只能和这个区间的长度有关。

Part 1

本题我们用分治思想入手。我们不妨按照时间分治。

倘若我们当前处理的操作区间为 [ L , R ] ( l e n = R − L + 1 ) [L,R](len=R-L+1) [L,R](len=RL+1)

不在这个区间内被修改的边的边权都是固定的。我们发现,对于[L,R]这个区间里的询问,除了在这个区间内有修改操作的边,还有一些边是一定没用的,有一些边是一定没用的。

我们记当前在这个询问区间内有n个点,m条边。

处理一定有用的边

然后我们再把所有修改的边的权值赋值为-INF(极小)(也就是尽量取这些边时),然后再做一遍最小生成树,那么这种情况下都选上的边后面一定用得上,将其权值计入答案,然后将这条边连接的两个点合成一个点。(那么此时点的规模最多为len)

处理一定没用的边

如果这个时候我们把所有修改的边的权值赋值为INF(也就是尽量不选),然后做一遍最小生成树,那么仍没选上的边一定用不到。(那么此时边的规模最多为2*len)

然后先处理当前分治区间,再处理左边的分治子区间,最后再处理右边的。如果分治到底层,那么将边权修改后进行最小生成树即可。(这个修改是全局的,故对于每一条边我们只存它对应原来的边的编号,用一个数组存边权即可)

Part 2

实际处理时的一些优化。

对于分治的每一层的边我们都可以存下来。

处理一定有用的边

我们先将要修改的边所连的两个点所在的集合都合并(也就是把他们都选上,如果已经在一个集合就不用),然后做一遍最小生成树,此时选中的边(不在这个区间里修改的)就是后面一定要用到的边。

将一定选的边的两个点合并可以用并查集。然后对更新每条边的端点即可。(也就是把两个点的编号变成一样的,有点不好描述,看代码好了)

处理一定没用的边

对于剩下的边按边权sort从小到大枚举,如果是要修改的边就直接选入边集并跳过(也就是尽量不选这些边),剩下的边正常做一遍最小生成树,将在最小生成树中的边选入边集。

Part 3

复杂度 O ( n log ⁡ ( n ) ) O(n\log(n)) O(nlog(n))

AC代码:

#include<cstdio>
#include<algorithm>
#include<cstring>
#define M 50005
using namespace std;
struct Line{
	int a,b,d,id;
	bool operator<(const Line &_)const{
		return d<_.d;
	}
}E[20][M];//每一层的边 
struct Op{
	int num,d;
}Q[M];
int fa[M];
int get_fa(int x){//并查集 
	if(x==fa[x])return x;
	return fa[x]=get_fa(fa[x]);
}
bool Merge(int x,int y){//如果两个点在同一个集合中返回false 
	int a=get_fa(x),b=get_fa(y);
	if(a==b)return false;
	fa[b]=a;
	return true;
}
void Init(Line *now,int tot){//预处理并差集 
	for(int i=1;i<=tot;i++){
		fa[now[i].a]=now[i].a;
		fa[now[i].b]=now[i].b;
	}
}
bool mark[M];
int num[M];//存一定在图中的边的编号 
int ID[M];
int val[M];
long long Ans[M];
void reduce_grath(Line *Lst,Line *now,int &tot,long long &sum){
	for(int i=1;i<=tot;i++)Lst[i].d=val[Lst[i].id];//注意此时可能每条边存的d不是当前的d(前面有修改) 
	sort(Lst+1,Lst+tot+1);
	Init(Lst,tot);
	for(int i=1;i<=tot;i++)if(mark[Lst[i].id])Merge(Lst[i].a,Lst[i].b);//将所有mark的边先尽量取 
	int sz=0;//sz的含义为一定要选的边的数量 
	for(int i=1;i<=tot;i++){
		if(Merge(Lst[i].a,Lst[i].b)){//如果可以加入图中,那么这个边一定不是被mark的 
			sum+=Lst[i].d;
			num[++sz]=i;
		}
	}
	Init(Lst,tot);
	for(int i=1;i<=sz;i++)Merge(Lst[num[i]].a,Lst[num[i]].b);
	//把这些边连着的两个点缩成一个点(也就是删除这条边) 
	for(int i=1;i<=tot;i++){
		ID[Lst[i].a]=get_fa(Lst[i].a);
		ID[Lst[i].b]=get_fa(Lst[i].b);
	}
	Init(Lst,tot);
	sz=0;//**sz的含义变为下一层的边的数量 
	for(int i=1;i<=tot;i++){
		int a=ID[Lst[i].a],b=ID[Lst[i].b];
		Line Nw=(Line){a,b,Lst[i].d,Lst[i].id};
		if(mark[Lst[i].id])now[++sz]=Nw;//如果是mark的边就直接选入边集 
		else if(Merge(a,b))now[++sz]=Nw;//剩下的边做最小生成树 
	}
	tot=sz;
}
void Solve(int L,int R,int tot,long long sum,int dep){//tot当前边数,sum当前已经选了边的边权和,dep当前处理的层的编号 
	if(L==R){
		val[Q[L].num]=Q[L].d;//修改边权 
		for(int i=1;i<=tot;i++)E[dep][i].d=val[E[dep][i].id];
		sort(E[dep]+1,E[dep]+tot+1);
		Init(E[dep],tot);
		for(int i=1;i<=tot;i++)if(Merge(E[dep][i].a,E[dep][i].b))sum+=E[dep][i].d;//最小生成树 
		Ans[L]=sum;
		return;
	}
	for(int i=L;i<=R;i++)mark[Q[i].num]=true;//将修改的边mark 
	reduce_grath(E[dep],E[dep+1],tot,sum);
	for(int i=L;i<=R;i++)mark[Q[i].num]=false;
	int mid=(L+R)>>1;
	Solve(L,mid,tot,sum,dep+1);//先处理左子区间 
	Solve(mid+1,R,tot,sum,dep+1);
}
int main(){
	int n,m,q;
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=m;i++)scanf("%d%d%d",&E[0][i].a,&E[0][i].b,&E[0][i].d),E[0][i].id=i,val[i]=E[0][i].d;
	for(int i=1;i<=q;i++)scanf("%d%d",&Q[i].num,&Q[i].d);
	Solve(1,q,m,0,0);
	for(int i=1;i<=q;i++)printf("%lld\n",Ans[i]);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值