【6*】图论拆点学习笔记
前言
WFLS 暑假集训 Day 11
此类知识点大纲中并未涉及,所以【6】是我自己的估计,后带星号表示估计,仅供参考。
图论拆点处理后效性和动态规划升维处理后效性有异曲同工之妙,所以以后动态规划的题我都用图论拆点做。
图论拆点
在动态规划中,我们通过升维来解决后效性。在图论中,如果在一个点上需要维护多个信息,并且这些信息共同决定最优解,我们可以效仿动态规划升维,使用图论拆点。
图论拆点主要有两种方式:
:拆点
图的点数为 ,假设我们需要在点 上额外维护一个信息 ,那我们可以把点 拆成点 ,分别对应动态规划升维后 各个状态。
优点:使用图论算法时较为简单,可以直接套模板求解。
缺点:抽象成动态规划类比时不够直观,有一定思维难度。
:分层图
条件同上,维护图上的点时,将图分为 层,第 层的点记作 ,则对应动态规划升维后 的状态。
优点:抽象成动态规划类比时比较直观,思维难度较低。
缺点:使用图论算法时较为复杂,多需要搭配哈希表。
图论和动态规划联系紧密。递推的动态规划相当于在一个 DAG 的拓扑序上求解,最短路径算法也可以看作在图论上用动态规划。换句话说,所有的动态规划题目,都可以通过图论拆点的方法解决。但是,教练曾有一句话:
不过有的时候没必要这么麻烦
例题
例题 :
由于在每个点上需要额外维护一个信息:到这个点用过的免费航班的次数 ,考虑图论拆点。
这里通过拆点实现,把点 拆成点 。对于一条权值为 有向边 ,如果不免费,那么 的值不会变化,同层转移,所以建边 ,边的权值为 。
如果免费,那么 的值加 ,边权值为 ,向上一层进行转移,所以建边 ,边的权值为 。
然后,由于要求最小值,在这个拆点之后的图上求单源最短路即可。
有一个坑点,记 为到点 的单源最短路径距离,最后求结果时不应该只求 ,应为:
因为有可能会 ,根本遍历不到 ,最多只会遍历到 。
#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;
}
例题 :
P1948 [USACO08JAN] Telephone Lines S
由于在每个点上需要额外维护一个信息:到这个点用过的免费电话线的次数 ,考虑图论拆点。
这里通过分层图实现。记第 层的第 个点为 ,考虑类比 Dijistra,记 为到第 层第 个点时所经过的路径上的边权的最大值的最小值,将求和改为求最小值。在堆中用 pair
维护点对 ,在每次出堆后,如果 ,遍历权值为 的边 ,则考虑两种转移:
:不使用免费边,由 , 和新边的权值 求最大值,与 求最小值。如果更小,则将 加入堆。
:使用免费边,由 , 和新边的权值 求最大值,与 求最小值。如果更小,则将 加入堆。
如果 ,则只需要考虑第一种情况,因为没有跟多免费线路可以使用了。
#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;
}
例题 :
由于在每个点上需要额外维护一个信息:到这个点的方向,考虑图论拆点。
这里通过拆点实现,把点 拆成点 和点 。其中点 表示方向为横,点 表示方向为纵。
建图时,将起始点 和结束点 加入换乘站序列,首先将每个点按照 排序,遍历每个点时,记录上一个与这个点的纵坐标 相等的点 ,在这两个点之间建边,边权为这两个点的 坐标之差的两倍,然后将 修改成这个点。然后按照 排序,类似上述操作,改为记横坐标相等的点,边权为这两个点的 坐标之差的两倍。并且这次建边时两个点的编号要加 ,表示方向为纵。
由于可以站内换乘改变方向,所以对于每个非起点和终点的点 建边 ,边权为 ,因为站内换乘改变方向需要一分钟。
由于需要求最快回家时间,由两个起点 和 为源点,直接跑一边单源最短路径,最后求 和 的最小值。
细节较多,实现时需要仔细。
#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;
}
后记
由于图论拆点内容不多,所以这篇学习笔记比较简短。
一句闲话:集训两个星期,讲了两个星期的图论,结果最后一场模拟赛一道图论都没有。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探