蓝桥杯----图论训练
STL
当想要维护一个数组,其中的元素要求有序,同时可能随时对这个数组中的元素进行增减
有没有一个STL可以快速维护一个这样的数组?
multiset(平衡二叉树)
默认从小到大排序
注意离散化中清除重复元素的原理:
unique()函数
vector中的earse是删除指定一段,所以离散化有:
《树的直径》
什么是树的直径?
在一颗树上,两个叶子节点之间的最长距离下的路径
证明方式<------
如何dfs?
void dfs(int node,int fa)
{
if (maxL<deep[node])
d=node,maxL=deep[node];
for (int i=0;i<sides[node].size();i++)
{
int child=sides[node][i];
if (child==fa) continue;
deep[child]=deep[node]+1;
road[child]=node;
dfs(child,node);
}
}
d为我们要求的端点,每一次只要深度有更深的点我们就更新
如何用拓扑排序的方式将叶子节点一圈一圈地给“减掉”?
双队列的方式,其中有个十分神奇的用法:
queue<int>q1,q2;
swap(q1,q2);
没想到swap可以直接交换队列
while (sum)
{
if (sum<=k)
break;
ans++;
while (q1.size())
{
int node=q1.front();
q1.pop();
sum--;
for (int i=0;i<sides[node].size();i++)
{
int next=sides[node][i];
if (du[next]<=1)
continue;
du[next]--;
if (du[next]==1)
q2.push(next);
}
}
swap(q1,q2);
}
《关于堆优化Dijkstra的st【】写法上的一点说明》
其实一般将这个st[]放到边的循环外面更好一点
主要是这样写简单,而且因为一般边的循环里面一般有判断
我们想要达到的目标是:
只是不让看过的点再次枚举他的边
而不是不能到这个点
还有写其他类似这中BFS的框架其实st【】放到哪里是不固定的
主要是看具体要求,有时这个st【】放到外面反而会错误
但是直接放外面简单
《最短路之分层最短路》
先通过一道题来看看什么是分层最短路:
题目链接<----
如果是题目输入建立出来的图就是如下:
然后我们将决策的代价作为边的值,将到达的状态作为点
形成了层次图
可以看到其实每层中基本边的连接和点是与各层是一样的
这里的模板我比较推崇:
#include<cstdio>
#include<cctype>
#include<cstring>
#include<queue>
#include<algorithm>
#include<vector>
#include<utility>
#include<functional>
int Read()
{
int x=0;char c=getchar();
while(!isdigit(c))
{
c=getchar();
}
while(isdigit(c))
{
x=x*10+(c^48);
c=getchar();
}
return x;
}
using std::priority_queue;
using std::pair;
using std::vector;
using std::make_pair;
using std::greater;
struct Edge
{
int to,next,cost;
}edge[2500001];
int cnt,head[110005];
void add_edge(int u,int v,int c=0)
{
edge[++cnt]=(Edge){v,head[u],c};
head[u]=cnt;
}
int dis[110005];
bool vis[110005];
void Dijkstra(int s)
{
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > points;
points.push(make_pair(0,s));
while(!points.empty())
{
int u=points.top().second;
points.pop();
if(!vis[u])
{
vis[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to;
if(dis[to]>dis[u]+edge[i].cost)
{
dis[to]=dis[u]+edge[i].cost;
points.push(make_pair(dis[to],to));
}
}
}
}
}
int main()
{
int n=Read(),m=Read(),k=Read(),s=Read(),t=Read();
int u,v,c;
for(int i=0;i<m;++i)
{
u=Read(),v=Read(),c=Read();
add_edge(u,v,c);
add_edge(v,u,c);
for(int j=1;j<=k;++j)
{
add_edge(u+(j-1)*n,v+j*n);
add_edge(v+(j-1)*n,u+j*n);
add_edge(u+j*n,v+j*n,c);
add_edge(v+j*n,u+j*n,c);
}
}
for(int i=1;i<=k;++i)
{
add_edge(t+(i-1)*n,t+i*n);
}//预防奇葩数据
Dijkstra(s);
printf("%d",dis[t+k*n]);
return 0;
}
这样的写法,首先就是按照层次图的样子将边建立好
然后直接跑Dijkstra,主要是因为这样简单好想
还一种写法是DP的写法,对于我来说如果条件复杂一点dp容易写错
这样的写法要注意的点是:
注意将全部的边都连好,不要漏边了,特别是在图是双向图的情况下
还是很有特征的:
进行决策,决策不影响图的结构,只影响当前的状态
层次图如下:一点都不层次
需要注意的是:
往下层走是单向的
下面这份代码是错误的,但是是给我做模板用的:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int N=10005,K=10;
struct node{
int nd,dt;
bool operator<(const node &t)const
{
return dt>t.dt;
}
};
priority_queue<node>pq;
vector<node>sides[N*K];
int dist[N*K],n,m;
bool st[N*K];
void Dijkstra()
{
memset(dist,0x3f,sizeof(dist));
pq.push({1,0});
dist[1]=0;
while (pq.size())
{
int fnd=pq.top().nd,fdt=pq.top().dt;
pq.pop();
if (st[fnd])
continue;
st[fnd]=true;
for (int i=0;i<sides[fnd].size();i++)
{
int cnd=sides[fnd][i].nd,cw=sides[fnd][i].dt;
if (dist[cnd]>dist[fnd]+cw)
{
dist[cnd]=dist[fnd]+cw;
pq.push({cnd,dist[cnd]});
}
}
}
}
int main()
{
cin>>n>>m;
for (int i=1;i<=m;i++)
{
int a,b,c,d;
cin>>a>>b>>c>>d;
if (d)
sides[a].push_back({b+n,c}),
sides[a+n].push_back({b+2*n,c}),
sides[b].push_back({a+n,c}),
sides[b+n].push_back({a+2*n,c});
else
sides[a].push_back({b,c}),
sides[b].push_back({a,c});
sides[a+n].push_back({b+n,c}),
sides[b+n].push_back({a+n,c}),
sides[a+2*n].push_back({b+2*n,c}),
sides[b+2*n].push_back({a+2*n,c});
}
Dijkstra();
cout<<dist[n]-min(dist[3*n],min(dist[n],dist[2*n]));
return 0;
}
《将题目图论化》
题目链接<-----
其实对于一个题目只要有类似节点(状态)和边的模型出来
就可以将其看成图来想了
这道题我们可以将数当成点,按钮按一下到另一个点,明显这个
这个可以想象成边,权重是1,
是1的权重用bfs最简单了
#include<iostream> #include<cstring> #include<algorithm> #include<vector> #include<queue> using namespace std; const int N=1e5+5; int n,k; int arr[N],ans=0,dist[N]; vector<int>sides[N]; bool st[N]; void bfs(int root) { memset(dist,0x3f,sizeof(dist)); queue<int>q; q.push(root); dist[root]=0; while (q.size()) { int node=q.front(); q.pop(); if (st[node]) continue; st[node]=true; ans=max(ans,dist[node]); for (int i=0;i<sides[node].size();i++) { int child=sides[node][i]; dist[child]=min(dist[child],dist[node]+1); q.push(child); } } } int main() { cin>>n>>k; for (int i=0;i<n;i++) arr[i]=i; for (int i=0;i<n;i++) { int to=arr[(i+1)%n],tw=arr[(i+k)%n]; sides[i].push_back(to),sides[i].push_back(tw); } bfs(0); cout<<ans<<endl; return 0; }
环问题
当看到环的时候应该要想到两个基本的解决方法:
1.并查集
2.拓扑
来看一道题目:
首先从并查集的角度来想吧:
想想并查集在kruskal算法的运用
我们在合并的时候是不是要用find()函数判断两个点的父节点是否是同一个?
如果是同一个,我们如果再合并这两个点(即将这两个点用边连起来)
是不是会形成环?
根据这个思想,我们可以不断地选择边,然后尝试将边上两个端点合并(即连接这两个端点)
直到遇到了两个端点(假设这两个端点为a,b)的父节点是一样,说明再合并就要成环了
我们不再合并,这个时候的图是颗树,我们知道会造成环的端点是a,b
因为在树上从一个端点到另一个端点的路径是唯一的
我们就可以从a dfs 到 b ,期间记录一下在这条路径上看过哪些点
这些点就是环上的点
#include<iostream> #include<cstring> #include<algorithm> #include<vector> using namespace std; const int N=1e5+5; int n,from[N],to[N],h[N]; vector<int>sides[N]; int find(int x) { if (h[x]!=x) h[x]=find(h[x]); return h[x]; } bool st[N]; void dfs(int sta,int fin) { if (sta==fin) { for (int i=1;i<=n;i++) if (st[i]) cout<<i<<" "; return ; } for (int i=0;i<sides[sta].size();i++) { int child=sides[sta][i]; if (st[child]) continue; st[child]=true; dfs(child,fin); st[child]=false; } } int main() { cin>>n; for (int i=1;i<=n;i++) { cin>>from[i]>>to[i]; h[i]=i; } for (int i=1;i<=n;i++) { int a=from[i],b=to[i]; int fa=find(a),fb=find(b); if (fa!=fb) { h[fb]=fa; sides[a].push_back(b),sides[b].push_back(a); } else { st[a]=true; dfs(a,b); break; } } return 0; }
在来从拓扑的角度来想:
我们不断地将没有环的节点拿掉,哪些那不掉的节点不就是环上的节点吗?
注意topsort的写法:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int N=1e5+5;
int n,rd[N];
vector<int>sides[N];
bool st[N];
void topsort()
{
queue<int>q;
for (int i=1;i<=n;i++)
if (rd[i]<=1)
{
q.push(i);
st[i]=true;
}
while (q.size())
{
int fa=q.front();
q.pop();
for (int i=0;i<sides[fa].size();i++)
{
int child=sides[fa][i];
if (st[child])
continue;
rd[child]--;
if (rd[child]<=1)
{
st[child]=true;
q.push(child);
}
}
}
}
vector<int>ans;
int main()
{
cin>>n;
for (int i=1;i<=n;i++)
{
int a,b;
cin>>a>>b;
rd[a]++,rd[b]++;
sides[a].push_back(b),sides[b].push_back(a);
}
//注意在topsort之前需要将全部的已经是叶子节点的点全部
//记录下来,否则会出错,而不是像下面这样只记录一个点
/*int sta;
for (int i=1;i<=n;i++)
if (rd[i]<=1)
{
sta=i;
break;
}*/
topsort();
for (int i=1;i<=n;i++)
if (!st[i])
ans.push_back(i);
sort(ans.begin(),ans.end());
for (int i=0;i<ans.size();i++)
cout<<ans[i]<<" ";
return 0;
}
模板错误记录
《LCA》
#include<iostream> #include<cstring> #include<algorithm> #include<vector> #include<queue> using namespace std; const int N=1e5+5; int n,m,root=1; vector<int>sides[N]; int lazy[N]; //需要注意的点:f[N][20]这样开是错误的, //我循环了20次,而这样数组下标就溢出了,会产生十分奇怪的错误 int deep[N],dist[N],f[N][25]; void bfs() { //这里deep一定要初始化为0x3f3f3f3f,因为下面LCA中防止跳出界限要用到 //同时主要要将0的deep初始化为0, memset(deep,0x3f,sizeof(deep)); queue<int>q; q.push(root); deep[0]=0,deep[root]=1,dist[root]=lazy[root]; while (q.size()) { int fa=q.front(); q.pop(); for (int i=0;i<sides[fa].size();i++) { int child=sides[fa][i]; if (deep[child]<deep[fa]) continue; deep[child]=deep[fa]+1; dist[child]=dist[fa]+lazy[child]; f[child][0]=fa; for (int j=1;j<=20;j++) //注意这里是j-1,而不是j/2 f[child][j]=f[f[child][j-1]][j-1]; q.push(child); } } } int LCA(int a,int b) { //a永远是深度更深的那个点 if (deep[a]<deep[b]) swap(a,b); //首先要将点a跳到与b点相同的深度 for (int i=20;i>=0;i--) { if (deep[f[a][i]]>=deep[b]) a=f[a][i]; } if (a==b) return a; //a与b一起跳 for (int i=20;i>=0;i--) { if (f[a][i]!=f[b][i]) a=f[a][i],b=f[b][i]; } return f[a][0]; } int main() { cin>>n>>m; for (int i=1;i<=n-1;i++) { int a,b; cin>>a>>b; sides[a].push_back(b),sides[b].push_back(a); lazy[a]++,lazy[b]++; } bfs(); while (m--) { int a,b; cin>>a>>b; int lca=LCA(a,b); cout<<dist[a]+dist[b]-2*dist[lca]+lazy[lca]<<endl; } return 0; }
<Floyd>
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N=205,INF=0x3f3f3f3f; int n,m,Time[N]; int dp[N][N][N]; void floyd() { for (int k=1;k<=n;k++) for (int i=1;i<=n;i++) for (int j=1;j<=n;j++) //注意这里是 dp[k-1][i][j] 而不是dp[k][i][j] dp[k][i][j]=min(dp[k-1][i][j], dp[k-1][i][k]+dp[k-1][k][j]); } int main() { cin>>n>>m; memset(dp,0x3f,sizeof(dp)); //注意如果这里最求模拟的真实性的话需要将自己到自己 //初始化为0 for (int i=0;i<=n;i++) for (int j=1;j<=n;j++) dp[i][j][j]=0; for (int i=1;i<=n;i++) cin>>Time[i]; for (int i=1;i<=m;i++) { int a,b,w; cin>>a>>b>>w; a++,b++; dp[0][a][b]=dp[0][b][a]=w; } floyd(); int q; cin>>q; while (q--) { int a,b,t; cin>>a>>b>>t; a++,b++; if (Time[a]>t || Time[b]>t) { cout<<-1<<endl; continue; } int maxn; for (int i=1;i<=n;i++) { if (Time[i]<=t) maxn=i; else break; } if (dp[maxn][a][b]<INF) cout<<dp[maxn][a][b]<<endl; else cout<<-1<<endl; } return 0; }
《topsort》
//注意在topsort之前需要将全部的已经是叶子节点的点全部
//记录下来,否则会出错,而不是像下面这样只记录一个点
/*int sta;
for (int i=1;i<=n;i++)
if (rd[i]<=1)
{
sta=i;
break;
}*/
topsort();