8.15 模拟赛记录 & 图论知识点补充

本文详细分析了图论中的欧拉路、二分图最大匹配问题,结合ACM竞赛题目,探讨了如何解决这类问题的策略与方法。通过实例解析,阐述了欧拉路的判断与构造,二分图的染色与分组背包策略,以及最大匹配的匈牙利算法。同时,分享了相关题目的解题思路与代码实现,帮助读者深入理解这些图论知识点。
摘要由CSDN通过智能技术生成

8.15 模拟赛记录 & 图论知识点补充

不得不感慨,写了好多天的总结,今天终于可以不用在写日期的时候自动-=1了(捂脸)

复盘

上来遍历。
T1首先想到了前两个字符作为一个元素和后两个字符匹配的思路,大概要上个哈希,然后写个暴搜,但是不好实现。T2第一眼以为是缩点,不知道咋从一个大的强连通分量里面割出一个小的,于是跳过。T3看了看比较复杂,估计要把询问离线,但是没想明白这样做的时间复杂度,但是突然想到或许可以用Floyd暴力比较容易的得到一些分(这是受了前天的启发),可以从这题开始做。T4第一眼看大概是最短路加上一些操作。
考虑到今天的暴力分特别多(240),所以策略直接变成写四个暴力,除了T2顺序写。T1的暴力之余用了一些哈希之类,不太好写,一直写到9点多。T3写的时候是考虑由简入难,先不考虑重边,用两个数组分别记一下i到j至少何时出发以及至少何时到达,很容易就可以转移,但是一旦出现重边就麻烦了,因为以上两个全都是“至少”,更新不了重边,这就会去掉一些合法情况,所以还得维护一个至多补缺。这时候已经觉得判断不是很严谨但是又不知道怎么弥补一下,不过自己出的有重边的数据过了,所以也就这样。T4仔细看了才发现其实就是Dijkstra的板子,在路径上的边权为1,不在就无限大(其实不建就行了),跑一遍最短路就是结果,很快收掉。此时大约是11:10.T2仔细看了一遍发现关系不具备传递性,和经典的团伙问题很像,于是上并查集,写了一堆特判过了样例。此时基本到点了,检查了一圈就上交了。
期望分数:60+30+60+100.

复盘分析

得分:0
怎么说呢,T3的判重确实有问题,确实应该上离线算法,这我无话可说,T1 T2其实考了两个我没学过的图,暴力不好写,写炸了,我也忍了,但是T4真的就是Dijkstra板子,前后犯了2个低级错误,一是边少存一次,二是在序列上但不是按序列顺序的边权还是应该赋成无限大,说白了只有在序列上顺序的是可用的,这俩错误不难发现,改过来就是100,所以跟预期差的很远是合理的,但是爆零是绝对不合理的。只能说是长个教训吧。

题解

T4已经解释过了,自环和非顺序的、至少一端不在序列上的不用存(存成最大的还浪费时间还浪费空间,毕竟开好几个long long),不贴代码了,属实有手就行,嘲讽一波自己。
T3需要进行离线,每次多一条边之后,其他所有点到这条边端点的最早到达时间都可能变化,如果反着存,那就变成了这两个端点到剩下所有联通的点的最早到达时间都可能变化,更好操作,所以反着存,把询问按出发时间降序排序,如果这段此时联通且最早到达时间少于询问的到达时间,就可以更新上询问的答案了。
其实跟图论自身知识点关系不大,主要就是考的离线。
完整代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct yjx{
	int from,to;
}a[2000001];
struct fxjtsl{
	int L,R,S,T,id;
}b[2000001];
bool cmp(fxjtsl x,fxjtsl y){
	return x.L > y.L;
}
int f[2000001],dis[1001][1001];
int cnt = 1,n,m,q;
int main(){
	//freopen("plan.in","r",stdin);
	//freopen("plan.out","w",stdout);
	int i,j,k,X,Y;
	scanf("%d %d %d",&n,&m,&q);
	memset(dis,0x3f,sizeof(dis));
	for(i = 1;i <= m;i++) scanf("%d %d",&a[i].from,&a[i].to);
	for(i = 1;i <= q;i++){
		scanf("%d %d %d %d",&b[i].L,&b[i].R,&b[i].S,&b[i].T);
		b[i].id = i;
	}
	sort(b + 1,b + q + 1,cmp);
	for(i = m;i >= 1;i--){
		X = a[i].from,Y = a[i].to;
		dis[X][Y] = dis[Y][X] = i;
		for(j = 1;j <= n;j++){
			dis[X][j] = dis[Y][j] = min(dis[X][j],dis[Y][j]);
		}
		while(cnt <= q && b[cnt].L == i){
			f[b[cnt].id] = (dis[b[cnt].S][b[cnt].T] <= b[cnt].R);
			++cnt;
		}
	}
	for(i = 1;i <= q;i++){
		if(f[i]) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}
/*
5 5 8
1 2
2 3
3 4
3 5
2 3
1 3 1 4
1 3 2 4
1 4 4 5
1 4 4 1
2 3 1 4
2 2 2 3
4 5 5 2
3 5 4 1
*/

T1的匹配规则就是一个三位字符串,前两个能和后两个匹配,把一个字符串前两位和后两位连起来,组成一个图,由于一条边代表的是一个字符串,必须走过所有的边且不重复走一条边(重边算多条边),这就形成一个欧拉路。字符串哈希之后进行下标对应就行了。

关于欧拉路的做法,在后文会有更详细的解释。

完整代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<map>
using namespace std;
const int N = 2e5 + 1;
const int mod = 1e7 + 7;
const int P = 131;
struct yjx{
	int nxt,to;
}e[N << 1];
int id[N];
int tot,top,n,ecnt = -1,delet[N],stak[N],head[N],in[N],out[N];
char s[N],s1[N][3];
void save(int x,int y){
	//printf("%d %d\n",x,y);
	e[++ecnt].nxt = head[x];
	e[ecnt].to = y;
	head[x] = ecnt;
	++in[y],++out[x];
}
void Gethash(){
	int x,y;
	x = (s[1] - '0' + 1) * P % mod + (s[2] - '0' + 1) % mod;
	y = (s[2] - '0' + 1) * P % mod + (s[3] - '0' + 1) % mod;
	if(!id[x]) id[x] = ++tot,s1[tot][1] = s[1],s1[tot][2] = s[2];
	if(!id[y]) id[y] = ++tot,s1[tot][1] = s[2],s1[tot][2] = s[3];
	save(id[x],id[y]);
}
void dfs(int now){
	int i,temp;
	for(i = delet[now];~i;i = delet[now]){
		delet[now] = e[i].nxt;
		temp = e[i].to;
		dfs(temp);
	}
	stak[++top] = now;
}
int main(){
	//freopen("recover.in","r",stdin);
	//freopen("recover.out","w",stdout);
	int i,t,p,st,ed,cnt;
	bool lipu;
	scanf("%d",&t);
	while(t--){
		memset(head,-1,sizeof(head));
		memset(in,0,sizeof(in));
		memset(out,0,sizeof(out));
		memset(id,0,sizeof(id));
		ecnt = -1;
		cnt = 0,lipu = 0,top = 0,tot = 0,st = 0,ed = 0;
		scanf("%d",&n);
		for(i = 1;i <= n;i++){
			scanf("%s",s + 1);
			Gethash();
		}
		for(i = 1;i <= tot;i++){
			if(abs(in[i] - out[i] > 1)) lipu = 1;
			if(in[i] != out[i]) ++cnt;
			if(in[i] - out[i] == 1) ed = i;
			if(out[i] - in[i] == 1) st = i;
		}
		if(cnt && cnt != 2 || lipu){
			printf("NO\n");
			continue;
		}
		if(!cnt) st = ed = 1;
		if(!st || !ed){
			printf("NO\n");
			continue;
		}
		for(i = 1;i <= tot;i++) delet[i] = head[i]; 
		dfs(st);
		if(top != n + 1){//不完全联通
			printf("NO\n");
			continue;
		}
		printf("YES\n");
		printf("%c",s1[st][1]);
		while(top){
			p = stak[top--];
			printf("%c",s1[p][2]);
		}
		printf("\n");
	}
	return 0;
}
/*
2
5
aca
aba
aba
cab
bac
4
abc
bCb
cb1
b13

4
aba
bab
cdc
dcd
*/

T2实际上是一个二分图问题,因为要求只能分成两组,不能处于同一组的两者连边,形成的就是一个二分图。
首先要确定确实是分成了两组且能分成两组,这个需要用二分图染色操作。考虑二分图和染色的性质,如果给左部的染a颜色,左部连接的右部就染b颜色,右部如果连接另一个左部就再给左部染a颜色…以此类推,在一个二分图上,只要我们给一个部的初始颜色一致,最终肯定是左边一种颜色,右边一种颜色,即使不是初始颜色一致,相连的点之间颜色也交替变化,这就可以dfs处理了。如果遇到已染色的点,其颜色与这次搜索将要染的颜色一样就返回1,不一样返回0(说明一个部内部连边了),给相连的节点染色也是一样的道理。
这题后面要用一下每个连通块里面多少个左部的点,多少个右部的点,所以顺手更新一下siz。
这部分代码如下:

bool dfs(int now,int col){
	int i;
	if(vis[now]){
	//这道题颜色后续得用作下标,所以用1和0表示,用了vis数组。实际染色用1和-1,为0说明没有染过色,可以不用vis数组。
		if(color[now] != col){
			return 0;
		}
		else return 1;
	}
	color[now] = col;
	vis[now] = 1;
	++siz[tot][col];
	for(i = 1;i <= n;i++){
		if(i == now) continue;
		if(!a[now][i] && !dfs(i,!col)) return 0;
	}
	return 1;
}
bool Graph(){
	int i;
	for(i = 1;i <= n;i++){
		if(!vis[i]){//图不一定整个联通
			++tot;
			if(!dfs(i,0)) return 0;//初始染色为0,可以不必分左右部。
		}
	}
	return 1;
}

处理出来siz,每次分组的时候就可以从每一块的每一种颜色里面选一个,最后希望选的这一组的个数最接近n/2,这就是一个分组背包的过程了,设dp[i][j]表示前i个连通块取j个点的方案是否存在,最后只需要取存在的dp[tot][i] (1<=i<=n)当中i最接近n/2的就行了,答案就是n-2*i.
这一部分代码如下:

dp[0][0] = 1;
for(i = 1;i <= tot;i++){
	for(j = n;j >= 0;j--){
		if(j >= siz[i][0] && dp[i - 1][j - siz[i][0]]) dp[i][j] = 1;
		if(j >= siz[i][1] && dp[i - 1][j - siz[i][1]]) dp[i][j] = 1;
	}
}
res = n;
for(i = 0;i <= n;i++){
	if(dp[tot][i]){
		res = min(res,abs(n - 2 * i));
	}
}
printf("%d\n",res);

此题额外需要注意的是,如果n=1要直接特判无解。

一些知识点补充

今天讲到两个以前不会的问题,一个是欧拉路,一个是二分图。以下整理两个今天做了的问题。

1.欧拉路问题

欧拉路就是从一个图上的某点,经过所有边能够经过所有点的方案,特别地,其中能回到出发点的称作欧拉回路(其实就是所谓一笔画问题)。

洛谷P7771 :给单向边,求一个字典序最小的欧拉路。

首先非常难受的是,为了字典序最小,肯定是某点的出边从小到大遍历,这是链式前向星做不到的,只能上vector了。
首先肯定想办法判断一下这图上是否有欧拉路。根据一笔画的知识,如果发现连了奇数条边的点不是0或2个,就不能一笔画出来(为0就是欧拉回路),在代码中,这可以表现为入度不等于出度,起点就取出度比入度多一的点,终点则正好相反(如果是0个,就直接把起点终点都设成1)。只要统计一下到底有多少这样的点、起点和终点是否存在就可以了。
求欧拉路的过程我们可以进行dfs,根据定义,我们先遍历所有的出边,在递归到下一层的时候记一下当前遍历到哪儿了,这样避免重复走,也节约了维护数组判断的空间。最后在所有出边都遍历结束后把这点入栈。
如果是先入栈后遍历,假设相连的点没有子节点,就会回溯,这时候相当于默认了可以走回父节点,但实际并不是这样。而如果最后入栈就避免了这个问题,因为越靠后遍历到的一定是越先入栈,就不存在所谓回溯问题。最后从栈里一个个取出来输出就行了。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 4e5 + 1;
vector<int> e[N];//不能用链前是真的难受
int in[N],out[N],st[N],stak[N],top;
void dfs(int x){
	int i;
	for(i = st[x];i < e[x].size();i = st[x]){
		st[x] = i + 1;//维护这一层下一个要遍历哪个点
		dfs(e[x][i]);
	}
	stak[++top] = x;
}
int main(){
	int i,n,m,x,y,s,t,cnt = 0;
	scanf("%d %d",&n,&m);
	for(i = 1;i <= m;i++){
		scanf("%d %d",&x,&y);
		e[x].push_back(y);
		++in[y],++out[x];
	}
	for(i = 1;i <= n;i++) sort(e[i].begin(),e[i].end());//注意结尾的下标是.end()-1
	s = 1;
	for(i = 1;i <= n;i++){
		if(in[i] != out[i]) ++cnt;
		if(in[i] - out[i] == 1) t = i;
		if(out[i] - in[i] == 1) s = i;
	}
	if(cnt && cnt != 2){
		printf("No\n");
		return 0;
	}
	if(!cnt) s = t = 1;
	if(!s || !t){
		printf("No\n");
		return 0;
	}
	dfs(s);
	if(top != m - 1){//不完全联通
		printf("No\n");
	} 
	while(top){
		printf("%d ",stak[top--]);
	}
	return 0;
} 
2.二分图最大匹配

这东西的相关应用我只是听dalao说了一嘴,具体的没去研究,回头研究了也补充在这儿。

二分图是什么上面T2题解已经解释过了。在一个边集(一个连通块里的全部边的集合)内部,不存在任何一对具有公共端点的边,就称这是一个匹配。

洛谷P3381:给定左右部各自的点数、所有的单向边(保证是一个二分图),求最大匹配。

这题已经给定了左右部各自的点数,我们首先用f[i][j]表示存在i到j的边,然后可以枚举左部里的点去搜索是否能形成一个匹配。
至于怎么求匹配,需要基于求一种叫增广路的东西,增广路大致的意思就是两个部之间的一个在两部之间交替连接,且不连重复的下标点的路径,例如下图:(图源:百度百科)
在这里插入图片描述
为了求得最大匹配,我们就要一直寻找所有的增广路,因为增广路上出现答案的机会是最多的。具体的说,从左部的某点开始搜索,如果过程中发现当前某个左部的点通向的右部的点已经被另一个左部的点匹配,那么就试着把这左部已匹配的点拿去匹配别的右部的点,而把现在这个左部的点匹配上(说白了其实就是试着挤一个位置出来)。这种算法称作匈牙利算法。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
bool f[2001][2001],vis[2001];
int n,m,match[2001];//右部的i与哪个左部的点匹配
bool find(int x){
	int i;
	for(i = 1;i <= m;i++){
		if(f[x][i]){
			if(vis[i]) continue;
			vis[i] = 1;
			if(!match[i] || find(match[i])){//i没有被匹配,或者原来匹配i的点可以和别的匹配
				match[i] = x;
				return 1;//多了一个匹配上的点
			}
		}
	}
	return 0;
}
int Hungary(){
	int i,ret = 0;
	memset(match,0,sizeof(match));
	for(i = 1;i <= n;i++){
		memset(vis,0,sizeof(vis));
		if(find(i)){
			++ret;
		}
	}
	return ret;
}
int main(){
	int i,x,y,s;
	scanf("%d %d %d",&n,&m,&s);
	for(i = 1;i <= s;i++){
		scanf("%d %d",&x,&y);
		f[x][y] = 1;
	}
	printf("%d",Hungary());
	return 0;
}

Thank you for reading!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值