【6*】图论拆点学习笔记

前言

WFLS 暑假集训 Day 11

此类知识点大纲中并未涉及,所以【6】是我自己的估计,后带星号表示估计,仅供参考。

图论拆点处理后效性和动态规划升维处理后效性有异曲同工之妙,所以以后动态规划的题我都用图论拆点做

图论拆点

在动态规划中,我们通过升维来解决后效性。在图论中,如果在一个点上需要维护多个信息,并且这些信息共同决定最优解,我们可以效仿动态规划升维,使用图论拆点

图论拆点主要有两种方式:

1:拆点

图的点数为 n,假设我们需要在点 i 上额外维护一个信息 k,那我们可以把点 i 拆成点 i,i+n,i+2ni+(k1)n,i+kn,分别对应动态规划升维后 f[i][0],f[i][1],f[i][2]f[i][k1],f[i][k] 各个状态。

优点:使用图论算法时较为简单,可以直接套模板求解。

缺点:抽象成动态规划类比时不够直观,有一定思维难度。

2:分层图

条件同上,维护图上的点时,将图分为 k 层,第 j 层的点记作 (i,j),则对应动态规划升维后 f[i][j] 的状态。

优点:抽象成动态规划类比时比较直观,思维难度较低。

缺点:使用图论算法时较为复杂,多需要搭配哈希表。

图论动态规划联系紧密。递推的动态规划相当于在一个 DAG 的拓扑序上求解,最短路径算法也可以看作在图论上用动态规划。换句话说,所有的动态规划题目,都可以通过图论拆点的方法解决。但是,教练曾有一句话:

不过有的时候没必要这么麻烦

例题

例题 1

P4568 [JLOI2011] 飞行路线

由于在每个点上需要额外维护一个信息:到这个点用过的免费航班的次数 c,考虑图论拆点。

这里通过拆点实现,把点 i 拆成点 i,i+n,i+2ni+(k1)n,i+kn。对于一条权值为 w 有向边 ij,如果不免费,那么 c 的值不会变化,同层转移,所以建边 ij,i+nj+n,i+2nj+2ni+(k1)nj+(k1)n,i+knj+kn,边的权值为 w

如果免费,那么 c 的值加 1,边权值为 0,向上一层进行转移,所以建边 ij+n,i+nj+2ni+(k1)nj+kn,边的权值为 0

然后,由于要求最小值,在这个拆点之后的图上求单源最短路即可。

有一个坑点,记 dis[i] 为到点 i 的单源最短路径距离,最后求结果时不应该只求 dis[t+k×n],应为:

mini=0k(dis[t+i×n])

因为有可能会 m<k,根本遍历不到 dis[t+k×n],最多只会遍历到 dis[t+m×n]

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	int v,dis,next;
}e[2400000];
int n,m,k,s,u,v,d,t,h[600000],dis[600000],book[600000],cnt=0,ans=99999999;
priority_queue<pair<int,int> >q;
void add_edge(int u,int v,int d)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=d;
	h[u]=cnt;
}

int main()
{
	scanf("%d%d%d",&n,&m,&k);
	scanf("%d%d",&s,&t);
	s++;t++;
	for(int i=1;i<=m;i++)
	    {
	    	scanf("%d%d%d",&u,&v,&d);
	    	u++;v++;
	    	for(int j=0;j<=k;j++)
	    	    {
	    	    add_edge(u+n*j,v+n*j,d),add_edge(v+n*j,u+n*j,d);
	    	    if(j<k)add_edge(u+n*j,v+n*(j+1),0),add_edge(v+n*j,u+n*(j+1),0);
	    	    }
		}
	for(int i=1;i<=n*(k+1);i++)dis[i]=99999999;
	dis[s]=0;
	for(int i=h[s];i;i=e[i].next)dis[e[i].v]=dis[s]+e[i].dis,q.push(make_pair(-dis[e[i].v],e[i].v));
	while(!q.empty())
	   {
	   	int now=q.top().second;
	   	q.pop();
	   	if(book[now])continue;
	   	book[now]=1;
	   	for(int i=h[now];i;i=e[i].next)
	   	    if(dis[now]+e[i].dis<dis[e[i].v]&&!book[e[i].v])dis[e[i].v]=dis[now]+e[i].dis,q.push(make_pair(-dis[e[i].v],e[i].v));
	   }
	for(int i=0;i<=k;i++)ans=min(ans,dis[t+n*i]);
	printf("%d",ans);
	return 0;
}

例题 2

P1948 [USACO08JAN] Telephone Lines S

由于在每个点上需要额外维护一个信息:到这个点用过的免费电话线的次数 c,考虑图论拆点。

这里通过分层图实现。记第 c 层的第 i 个点为 (i,c),考虑类比 Dijistra,记 dis[i][c] 为到第 c 层第 i 个点时所经过的路径上的边权的最大值的最小值,将求和改为求最小值。在堆中用 pair 维护点对 (i,c),在每次出堆后,如果 c<k,遍历权值为 w 的边 ij,则考虑两种转移:

1:不使用免费边,由 (i,c)(j,c)dis[i][c] 和新边的权值 w 求最大值,与 dis[j][c] 求最小值。如果更小,则将 (j,c) 加入堆。

2:使用免费边,由 (i,c)(j,c+1)dis[i][c] 和新边的权值 0 求最大值,与 dis[j][c] 求最小值。如果更小,则将 (j,c+1) 加入堆。

如果 c=k,则只需要考虑第一种情况,因为没有跟多免费线路可以使用了。

#include <bits/stdc++.h>
using namespace std; 
struct edge
{
	int next,t,dis;
}e[30000];
int n,m,k,h[30000],dis[1001][1001],book[1001][1001],cnt=0,ans=99999999;
priority_queue<pair<int,pair<int,int> > >q;
void add_edge(int u,int v,int dis)
{
	e[++cnt].next=h[u];
	e[cnt].t=v;
	e[cnt].dis=dis;
	h[u]=cnt;
}

int main()
{
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=m;i++)
	    {
	    	int u,v,dis;
	    	scanf("%d%d%d",&u,&v,&dis);
	    	add_edge(u,v,dis);
	    	add_edge(v,u,dis);
		}
	for(int i=2;i<=n;i++)
	    for(int j=0;j<=k;j++)
	        dis[i][j]=99999999;
	for(int i=0;i<=k;i++)
	    dis[1][i]=0,book[1][i]=1;
	for(int i=h[1];i;i=e[i].next)
	    {
	    dis[e[i].t][0]=e[i].dis;
	    q.push(make_pair(-dis[e[i].t][0],make_pair(e[i].t,0)));
	    for(int j=1;j<=k;j++)dis[e[i].t][j]=0,q.push(make_pair(-dis[e[i].t][j],make_pair(e[i].t,j)));
	    }
	while(!q.empty())
	    {
	    	int t=q.top().second.first,s=q.top().second.second;
	    	q.pop();
	    	if(book[t][k])continue;
	    	for(int i=h[t];i;i=e[i].next)
	    	    {
	    	    if(max(dis[t][s],e[i].dis)<dis[e[i].t][s])dis[e[i].t][s]=max(dis[t][s],e[i].dis),q.push(make_pair(-dis[e[i].t][s],make_pair(e[i].t,s)));
	    	    if(s<k&&dis[t][s]<dis[e[i].t][s+1])dis[e[i].t][s+1]=dis[t][s],q.push(make_pair(-dis[e[i].t][s+1],make_pair(e[i].t,s+1)));
	    	    }
		}
	for(int i=0;i<=k;i++)
	    ans=min(ans,dis[n][i]);
	if(ans!=99999999)printf("%d",ans);
	else printf("-1");
	return 0;
}

例题 3

P3831 [SHOI2012] 回家的路

由于在每个点上需要额外维护一个信息:到这个点的方向,考虑图论拆点。

这里通过拆点实现,把点 i 拆成点 i 和点 i+n。其中点 i 表示方向为横,点 i+n 表示方向为纵。

建图时,将起始点 s 和结束点 t 加入换乘站序列,首先将每个点按照 x 排序,遍历每个点时,记录上一个与这个点的纵坐标 y 相等的点 h[y],在这两个点之间建边,边权为这两个点的 x 坐标之差的两倍,然后将 h[y] 修改成这个点。然后按照 y 排序,类似上述操作,改为记横坐标相等的点,边权为这两个点的 y 坐标之差的两倍。并且这次建边时两个点的编号要加 n,表示方向为纵。

由于可以站内换乘改变方向,所以对于每个非起点和终点的点 i 建边 ii+n,i+ni,边权为 1,因为站内换乘改变方向需要一分钟。

由于需要求最快回家时间,由两个起点 ss+n 为源点,直接跑一边单源最短路径,最后求 dis[t]dis[t+n] 的最小值。

细节较多,实现时需要仔细。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
	int v,dis,next;
}e[1800000];
struct transport
{
	int x,y,p;
}di[300000];
int n,m,sx,sy,tx,ty,sh,th,u,v,x[300000],y[300000],h[300000],c1[300000],c2[300000],r1[300000],r2[300000],dis[300000],book[300000],cnt=0,ans=99999999;
priority_queue<pair<int,int> >q;
bool cmp1(struct transport a,struct transport b)
{
	return a.x<b.x;
}

bool cmp2(struct transport a,struct transport b)
{
	return a.y<b.y;
}

void add_edge(int u,int v,int d)
{
	e[++cnt].next=h[u];
	e[cnt].v=v;
	e[cnt].dis=d;
	h[u]=cnt;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	    {
	    scanf("%d%d",&di[i].x,&di[i].y);
	    di[i].p=i;
	    }
	scanf("%d%d%d%d",&sx,&sy,&tx,&ty);
	di[++m].x=sx,di[m].y=sy,di[m].p=m,sh=m;
	di[++m].x=tx,di[m].y=ty,di[m].p=m,th=m;
	sort(di+1,di+m+1,cmp1);
	for(int i=1;i<=m;i++)
	    {
	    if(c1[di[i].y])add_edge(c1[di[i].y],di[i].p,(di[i].x-di[c2[di[i].y]].x)*2),add_edge(di[i].p,c1[di[i].y],(di[i].x-di[c2[di[i].y]].x)*2);
	    c1[di[i].y]=di[i].p;c2[di[i].y]=i;
	    }
	sort(di+1,di+m+1,cmp2);
	for(int i=1;i<=m;i++)
	    {
	    if(r1[di[i].x])add_edge(r1[di[i].x]+m,di[i].p+m,(di[i].y-di[r2[di[i].x]].y)*2),add_edge(di[i].p+m,r1[di[i].x]+m,(di[i].y-di[r2[di[i].x]].y)*2);
		r1[di[i].x]=di[i].p;r2[di[i].x]=i;
	    }
	for(int i=1;i<=m;i++)
	    if(i!=sh&&i!=th)add_edge(i,i+m,1),add_edge(i+m,i,1);
	for(int i=1;i<=2*m;i++)dis[i]=99999999;
	dis[sh]=0;dis[sh+m]=0;
	for(int i=h[sh];i;i=e[i].next)dis[e[i].v]=dis[sh]+e[i].dis,q.push(make_pair(-dis[e[i].v],e[i].v));
	for(int i=h[sh+m];i;i=e[i].next)dis[e[i].v]=dis[sh+m]+e[i].dis,q.push(make_pair(-dis[e[i].v],e[i].v));
	while(!q.empty())
	   {
	   	int now=q.top().second;
	   	q.pop();
	   	if(book[now])continue;
	   	book[now]=1;
	   	for(int i=h[now];i;i=e[i].next)
	   	    if(dis[now]+e[i].dis<dis[e[i].v]&&!book[e[i].v])dis[e[i].v]=dis[now]+e[i].dis,q.push(make_pair(-dis[e[i].v],e[i].v));
	   }
	ans=min(dis[th],dis[th+m]);
	if(ans==99999999)printf("-1");
	else printf("%d",ans);
	return 0;
}

后记

由于图论拆点内容不多,所以这篇学习笔记比较简短。

一句闲话:集训两个星期,讲了两个星期的图论,结果最后一场模拟赛一道图论都没有。

posted @   w9095  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示