浅析NOIP中的图论【1】

1.最短路的变式

(1)逆向思维 

请思考:给一个有向图,单源最短路径算法(dij,spfa)可以很轻松处理从单起点到多终点的最短距离,那么,如果要求多起点到单终点的最短距离呢?(不要说跑n次dij)

分析:假设要问从x到1的最短路,为x->a->b->c->1,也就是说x->a,a->b,b->c,c->1都有路可走,那么我们想想,从x开始x->a,a->b,b->c,c->1的最短路不就是从1开始1->c,c->b,b->a,a->x的最短路吗?于是这时,我们把x->a,a->b,b->c,c->1这4条路径变为1->c,c->b,b->a,a->x,然后从1开始跑最短路,而它们的最短路是一样的

总结:我们把这样从多到一的最短路变式的路径反转操作称作反向建边

(附建反边最短路模板题练手):Cow Party S 、邮递员送信  、请柬  (三倍经验)

那么,noip的考题中是如何运用建反边思想的呢?

[NOIP2014 提高组] 寻找道路

题意:找到从x到y的最短路,要求路径上的所有点的出边所指向的点都直接或间接与终点连通

思考:

做什么?要求路径上所有点与终点连通--->删除不与终点连通的点,以及与他们连一条边的点

怎么做?要找倒着从终点出发,把图搜完到不了的点--->反向建边,给搜索到的点打标记,然后枚举所有没打标记的点,枚举他们的连边消去标记

<细节>注意要另开一个标记数组,因为直接消去标记的话会对后面产生影响,删除一个不与终点连接的点后,下一次搜到那个点还会删除一次

最后跑一遍最短路即可

#include <bits/stdc++.h>
using namespace std;
int n,m,x,y,s,t,head[500005],cnt,dis[500005];
bool vis[500005],vis2[500005],okk[500005],ok[500005];
queue<int> q,qq;
struct node{int to,nxt;}e[500005];
void insert(int u,int v){
	e[++cnt].nxt=head[u];e[cnt].to=v;head[u]=cnt;
}
void bfs(){
	ok[t]=1;qq.push(t);vis[t]=1;
	while(!qq.empty()){
		int u=qq.front();qq.pop();
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if(!vis2[v]){
				vis2[v]=1;ok[v]=1;qq.push(v);
			}
		}
	}
}
void spfa(){
	for(int i=1;i<=n;i++)dis[i]=1e9;
	dis[t]=0;vis[t]=1;q.push(t);
	while(!q.empty()){
		int u=q.front();q.pop();vis[u]=0;
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if(dis[v]>dis[u]+1&&ok[v]){
				dis[v]=dis[u]+1;
				if(!vis[v]){
					q.push(v);vis[v]=1;
				}
			}	
		}	
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);insert(y,x);
	}
	scanf("%d%d",&s,&t);
	bfs();
	for(int i=1;i<=n;i++)okk[i]=ok[i];
	for(int i=1;i<=n;i++){
		if(!okk[i]){
			for(int j=head[i];j;j=e[j].nxt){
				if(ok[e[j].to])ok[e[j].to]=0;
			}
		}
	}
	spfa();
	if(dis[s]>=1e9)printf("-1\n");
	else printf("%d\n",dis[s]);
	return 0;
}

反思:这道题我们从“合法点需与终点连通” 入手,想到从终点出发搜到的点即为合法点,故反向建边搜索打标记(注意备份数组的细节)  


(2)dp状态的设定技巧与0环的处理

[NOIP2017 提高组] 逛公园   

题意:统计从1-n长度<=d+k的路径条数

#include <bits/stdc++.h>
using namespace std;
int n,m,k,p,head[100005],resav[100005],dis[100005],d,timber,tot;
int ans,fa[100005],s[100005],top,low[100005],dfn[100005],t;
int f[100005][52],sz[100005],flag;
bool vis[100005],vist[100005][52],visit[100005];
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
struct node{int to,nxt,w;}e[200005];
void insert(int u,int v,int w){
	e[++tot].nxt=head[u];e[tot].to=v;e[tot].w=w;head[u]=tot;
}
void dij(){
	vis[1]=dis[1]=0;q.push(make_pair(0,1));
	while(!q.empty()){
		int u=q.top().second;q.pop();
		if(vis[u])continue;vis[u]=1;
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to,w=e[i].w;
			if(dis[v]>dis[u]+w)dis[v]=dis[u]+w,q.push(make_pair(dis[v],v));
		}
	}
}
void tarjan(int u){
	dfn[u]=low[u]=++timber;s[++top]=u;visit[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		if(e[i].w)continue;
		int v=e[i].to;
		if(!dfn[v])tarjan(v),low[u]=min(low[u],low[v]);
		else if(visit[v])low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		while(top){
			int x=s[top--];visit[x]=0;
			fa[x]=u;sz[u]++;
			if(x==u)break;
		} 
	}
}
void check(){
	timber=0; for(int i=1;i<=n;i++)dfn[i]=low[i]=sz[i]=0,fa[i]=i;
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
}
int dfs(int x,int y){
	if(y>k)return 0;
	if(vist[x][y])return f[x][y];
	vist[x][y]=1;f[x][y]=0;
	if(x==n)f[x][y]++;
	for(int i=head[x];i;i=e[i].nxt){
		int xp=dfs(e[i].to,y+dis[x]+e[i].w-dis[e[i].to]);
		if(xp&&sz[fa[e[i].to]]>1){flag=1;return 0;}
		f[x][y]=(f[x][y]+xp)%p;
	}
	return f[x][y]%p;
}
int main(){
	scanf("%d",&t);
	while(t--){
		ans=0;scanf("%d%d%d%d",&n,&m,&k,&p);
		flag=0;memset(head,0,sizeof(head));tot=0;
		memset(vist,0,sizeof(vist));
		for(int i=2;i<=n;i++)vis[i]=0,dis[i]=1e9;
		for(int i=1,x,y,w;i<=m;i++)scanf("%d%d%d",&x,&y,&w),insert(x,y,w);
		check();dij(); int res=dfs(1,0);
		if(flag)printf("-1\n");
		else printf("%d\n",res);
	}
	return 0;
}

2.贪心的思想

贪心是很普遍的思想,且更多的是结合在整个做法中的一小部分,很少单独考贪心,这里不再赘述,主要引领贪心在noip图论题里的思考方向 并分析贪心的策略

t1:[NOIP2018 提高组] 旅行

题意:给定一个无向连通图,从任意点开始,按以下方式进行遍历1~n:沿着第一次到达该点的边进行回溯(当然起点不能回溯),或者沿着相连的一条边到达一个未到达过的点,要求使遍历序列的字典序尽量小 (注:60pts是树,100pts是基环树)

(1)60pts 

做什么?求字典序最小--->贪心,从1号节点开始,每次走编号最小的一条

怎么做?用vector存图(方便sort)对每个节点把所有它连接的点编号从小到大排序,dfs一遍

(2)100pts

思考:如果一个图里有环的话(n≤m在没有重边自环的情况下是肯定有环的),这一个环里肯定有一条边是用不到的,正确性显而易见,因为我们下一步只能往没走过的地方走,或者是朝上一个地方走,在这种情况下我们能走的点是n,而能经过的边只有n-1条,对于一个x个点x个边的环来说,肯定是有一条边用不到的(基环树嘛),所以我们就枚举这条用不到的边,然后跑Dfs即可

#include <bits/stdc++.h>
using namespace std;
int n,m,tmp[5005],ans[5005],head[5005],cnt,dep,dep2,su,sv;
struct node{int nxt,u,v;}e[10005];
bool vis[5005];
vector<int> ver[5005];
void insert(int u,int v){
e[++cnt].nxt=head[u];e[cnt].v=v;e[cnt].u=u;head[u]=cnt;}
void dfs1(int u,int fa){
	if(vis[u])return;
	vis[u]=1;ans[++dep]=u;
	for(int i=0;i<ver[u].size();i++){
		int v=ver[u][i];
		if(v==fa)continue;
		dfs1(v,u);
	}
}
void dfs2(int u,int fa){
	if(vis[u])return;
	vis[u]=1;tmp[++dep2]=u;
	for(int i=0;i<ver[u].size();i++){
		int v=ver[u][i];
		if(v==fa)continue;
		if((v==sv&&u==su)||(v==su&&u==sv))continue;
		dfs2(v,u);
	}
}
void renew(){for(int i=1;i<=n;i++)ans[i]=tmp[i];}
bool check(){
	for(int i=1;i<=n;i++){
		if(tmp[i]==ans[i])continue;
		if(tmp[i]>ans[i])return 0;
		else return 1;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
	int x,y;scanf("%d%d",&x,&y);
	ver[x].push_back(y);ver[y].push_back(x);insert(x,y);insert(y,x);
	}
	for(int i=1;i<=n;i++)sort(ver[i].begin(),ver[i].end());
	if(n==m-1){
		dfs1(1,0);for(int i=1;i<=n;i++)printf("%d ",ans[i]);
	}else{
		for(int i=0;i<cnt;i+=2){
			dep2=0;su=e[i].u;sv=e[i].v;
            memset(vis,0,sizeof(vis));
			dfs2(1,0);
			if(dep2<n)continue;//如果删边不联通了就蒜了
			if(!ans[1])renew();
			else if(check())renew();
		}
		for(int i=1;i<=n;i++)printf("%d ",ans[i]);
	}
	return 0;
}

 t2: [NOIP2018 提高组] 赛道修建​​​​​​

题意:一个n个节点的树,需要在树上找出m条边不相交的路径,使得路径的最小值最大

(1)做什么?最大化最小值--->二分,转为判定问题:二分枚举k能否选出m条长度不小于k的路径--->给出k,求不小于k的路径最多有多少条--->贪心

(2)怎么做?从整体到局部可分为step123

step1怎么让不小于k的路径尽量多?---先思考贪心策略

定义:从 u子树中某个节点连向 u的一条路径称为“半链”

  • 每次合并尽量让两条半链总长接近 k更优--->让合法的总长尽量小更优

  • 若 v是 u的儿子,如果 v的子树中有两条半链可以合并,那么就不要将其中某一条(再加上 u,v 距离后)留到 u处合并。因为一条半链对答案最多造成 1的贡献,而且 v 只能留出一条返回给 u(划重点 后面总结也会提到)。--->能在子树中合并就尽量在子树中合并

step2递归遍历整棵树时具体如何处理?---再思考各种情况下递归返回值

访问 u号节点时,先将 u的儿子们的子树中能够合并的半链尽量合并,并将合并的数量累加到答案中。从某个儿子返回时,无非就两种情况:

  1. 该儿子子树中的半链不能两两配对合并。要么剩余的半链长度太小,要么数量不够(是奇数)
  2. 子树中的半链已经两两配对了,没有剩余的半链。

相应的解决方案如下:

  1. 在剩余的半链中选择一条最长的半链,将其长度加上 u, v 距离后返回。(一个儿子最多返回一条半链)
  2. 直接返回 u, v距离。

step3具体如何合并半链?---最后思考合并方法

每次找到两个总和不小于k的数据,将答案累加 1,并将这两个数据删除--->multiset

multiset::lower_bound(x) 返回第一个大于等于 x的数 y(的迭代器)

<细节>如果某条半链的长度>=k,则这条半链可以单独成为一条路径(不用合并)

#include <iostream>
#include <set>
#include <cstring>
using namespace std;
struct node{int nxt,to,w;}e[100005];
int head[100005],cnt,f[100005],g[100005],n,m,l=1,r,mid,ans;
multiset<int> s;
multiset<int> :: iterator it;
inline void insert(int u,int v,int w){
	e[++cnt].nxt=head[u];e[cnt].to=v;e[cnt].w=w;head[u]=cnt;
}
inline void dfs(int u,int fa){
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		f[u]+=f[v];
	}
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa)continue;
		s.insert(e[i].w+g[v]);
	}
	while(s.size()){
		int now=*--s.end();
		if(now>=mid){
			f[u]++;
			s.erase(--s.end());
		}else break;
	}
	while(s.size()){
		int now=*s.begin();
		s.erase(s.begin());
		it=s.lower_bound(mid-now);
		if(it==s.end())g[u]=now;
		else f[u]++,s.erase(it);
	}
}
int main(){
	scanf("%d%d",&n,&m); 
	for(int i=1;i<=n-1;i++){
		int x,y,z;scanf("%d%d%d",&x,&y,&z);
		insert(x,y,z);insert(y,x,z);r+=z;
	}
	while(l<=r){
		memset(f,0,sizeof(f));memset(g,0,sizeof(g));
		mid=(l+r)>>1;
		dfs(1,0);
		if(f[1]>=m)ans=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",ans);
	return 0;
}

 反思:这种“既然贡献都为1,不如先选满足条件下最劣的,剩下较优的以对后续有利”的贪心思想是比较常见的,这里提供思路相近的一道题供练习[HEOI2015]兔子与樱花 


t3:[CSP-S2019] 树上的数​​​​​​

题意:给定一棵树和每个节点上的初始数字,删除一条边的效果是交换被这条边连接的两个节点上的数字交换,设pi表示数字i在哪个节点,要求合理安排删除n-1条边的顺序,使得排列p字典序最小

(1)考虑把一个数字从一个节点运送到另一个节点的过程。

可以发现,限制总共有3种:

  • 对于起点,选择的边要求是这个点所有边中第一条删除的;
  • 对于中间的点,要求选择的两条边的删除顺序必须连续;
  • 对于终点,选择的边要求是这个点所有边中最后一条删除的。

--->(2)那么考虑对于每个点建立一张图,图上面的点代表这个点连出去的一条边。

在这张图上的一条有向边代表出点要在入点之后马上选择,同时记录这个点钦定的第一条边和最后一条边。每个点的图不相关。

--->(3)那么考虑贪心,从小到大确定每个数字的最终位置,同时保证不矛盾。

--->(4)矛盾的情况有三种:

  • 图的形式不是若干条链的形式;
  • 第一个点(边)有入边,最后一个点(边)有出边;
  • 第一个点(边)所在的链的链尾是最后一个点(边),但是还有其他的点不在链中。

从每个数字的起始点出发,保证从根到这个点的路径不会引起矛盾,更新答案即可。

可通过并查集实现,如果想写O(n^2)的链表写法,那么只要满足每次相连的是两条链的链首和链尾,然后分别记录链首、链尾的所在的链的链尾和链首即可。


 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值