6491. 【GDOI2020模拟03.04】铺路

题目

有一个图,每次加入一条边,对于每个加入的边分别输出加入这条边之后,加入的所有边中选择若干条使得所有点的度数为奇数,用到的边最长是多少。


思考历程

比赛时是推出了很多条性质的。
但是一开始就理解错了题意,题目中的“道路”我理解成了路径,实际上是边。
由于经过这种想法改过的题意过于毒瘤,我几乎没有思路。
最终打了暴力上去,居然能水5分。


正解

有条性质:
一个连通块符合条件,当且仅当这个连通块的总点数为偶数。
(这个连通块是只要用的和不用的边都加进去形成的连通块)
首先,通过度数的奇偶性,可以发现把用到的边都加进去之后,最终形成的都是偶数连通块。
考虑在偶数联通块中构造一个合法的方案:建立一棵生成树,随便选一个点作为根,从叶子往上做。首先选择叶子的父亲边,保证叶子的度数为奇数。对于上面的每个点,看看有多少个通向儿子的边被选了,根据它的奇偶性来决定它到父亲的边选不选。
最终只剩下根节点。我们可以通过度数总和来计算出根节点的度数,发现这时候它也是奇数度数的。
这样就证完了。

这条性质是这道题的灵魂,有了它,意味着我们需要选择一些边,使得图分成若干个偶数连通块。这些边可能在最终的答案中不会用到,但没有关系。这时候答案为最长边。
可以这样暴力:直接最小生成树,维护奇数连通块的个数,第一次等于零的时候输出最后加入的那条边。

由此延伸出了无数种做法,比如可以随便LCT撵爆。
讲个线段树分治。
对于一条边,考虑它在那些询问中被选择,容易发现这是一段区间,头是它加入的时候,我们试着计算出尾。
用一个链表从小到大存下所有边,表示没有确定出尾的位置的边有哪些。
线段树分治的时候从后往前做。到叶子节点的时候,如果当前仍然存在奇数区间,那就将链表中的边从头到尾扔进图中,直到不存在奇数区间为止。这些边在这个时候已经确定了它们的尾,所以把他们从并查集中删去,并在它们影响到的区间上打上标记。
进入一个节点时将标记的边加入图中,退出的时候撤销。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <cassert>
#include <algorithm>
#define N 100010
#define M 300010
int n,m;
struct edge{
	int u,v,w;
} ed[M];
int q[M],nxt[M];
bool cmpq(int a,int b){return ed[a].w<ed[b].w;}
struct EDGE{
	int id;
	EDGE *las;
} e[M*40];
int ne;
EDGE *last[M*4];
int odd;
int fa[N],siz[N];
int getfa(int x){return fa[x]==x?x:getfa(fa[x]);}
int opt[N],cnt;
void clear(int btm){
	for (;cnt>btm;--cnt){
		int u=opt[cnt],v=fa[u];
		odd=odd-(siz[v]&1)+(siz[u]&1)+(siz[v]-siz[u]&1);
		siz[v]-=siz[u];
		fa[u]=u;
	}
}
int ans[M];
void cover(int k,int l,int r,int st,int en,int x){
	if (st<=l && r<=en){
		e[ne]={x,last[k]};
		last[k]=e+ne++;
		return;
	}
	int mid=l+r>>1;
	if (st<=mid)
		cover(k<<1,l,mid,st,en,x);
	if (mid<en)
		cover(k<<1|1,mid+1,r,st,en,x);
}
void calc(int k,int mx){
	int tmp=cnt,x=nxt[0],lst=0;
	for (;odd && x<=m;lst=x,x=nxt[x]){
		nxt[0]=nxt[x];
		if (q[x]>k)
			continue;
		if (q[x]<=k-1)
			cover(1,1,m,q[x],k-1,q[x]);
		mx=max(mx,ed[q[x]].w);
		int u=getfa(ed[q[x]].u),v=getfa(ed[q[x]].v);
		if (u!=v){
			odd=odd-(siz[u]&1)-(siz[v]&1)+(siz[u]+siz[v]&1);
			if (siz[u]>siz[v])
				swap(u,v);
			fa[u]=v;
			siz[v]+=siz[u];
			opt[++cnt]=u;
		}
	}
	if (odd)
		ans[k]=-1;
	else
		ans[k]=mx;
	clear(tmp);
}
void dfs(int k,int l,int r,int mx){
	int tmp=cnt;
//	printf("%d %d\n",k,last[k]);
	for (EDGE *ei=last[k];ei;ei=ei->las){
		int u=getfa(ed[ei->id].u),v=getfa(ed[ei->id].v);
		mx=max(mx,ed[ei->id].w);
		if (u==v)
			continue;
		odd=odd-(siz[u]&1)-(siz[v]&1)+(siz[u]+siz[v]&1);
		if (siz[u]>siz[v])
			swap(u,v);
		fa[u]=v;
		siz[v]+=siz[u];
		opt[++cnt]=u;
	}
	if (l==r){
		calc(l,mx);
		clear(tmp);
		return;
	}
	int mid=l+r>>1;
	dfs(k<<1|1,mid+1,r,mx);
	dfs(k<<1,l,mid,mx);
	clear(tmp);
}
int main(){
	freopen("road.in","r",stdin);
	freopen("road.out","w",stdout);
	scanf("%d%d",&n,&m);
	for (int i=1;i<=m;++i)
		scanf("%d%d%d",&ed[i].u,&ed[i].v,&ed[i].w),q[i]=i;
	sort(q+1,q+m+1,cmpq);
	for (int i=0;i<=m;++i)
		nxt[i]=i+1;
	odd=n;
	for (int i=1;i<=n;++i)
		fa[i]=i,siz[i]=1;
	dfs(1,1,m,0);
	for (int i=1;i<=m;++i)
		printf("%d\n",ans[i]);
	return 0;
}

总结

如果比赛时看懂了题意,说不定当时就想出来了呢……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值