图论算法复习QwQ
最短路问题及其相关
Floyd及其相关
floyd适用于稠密图,存图方式是邻接矩阵,每个点到其他所有点都有存边。
至于floyd的k为什么要在最外层,是因为Floyd算法的本质是DP,而k是DP的阶段,因此要写最外面。
floyd的DP本质:
其原理是经过k点是否可以缩短路径。
原转移方程是这样的:
f[k][i][j]表示i到j之间通过编号k松弛出的最短路,f[0][i][j]为原图的临界矩阵。
这个状态设计有三维,
f[k][i][j]=min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]),前者表示不从k点走,后者表示从k点走,因为f[k]只与f[k-1]有关,所以就省略了。
模板:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=1001;
const int maxm=1000*1000+1;//边数,即点数的平方
const int inf=1<<29;//求最大值时 是-inf
int n,m,a[maxm],f[maxn][maxn],ans,u,v,w;
void init(){
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i==j) f[i][j]=0;
else f[i][j]=inf;
}
void floyd(){
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
}
int main()
{
scanf("%d%d",&n,&m);
init();//记得初始化
for(int i=1;i<=n;i++) //邻接矩阵
for(int j=1;j<=n;j++){
scanf("%d",&w);
f[i][j]=mid(w,f[i][j]);//注意判断一下充边,避免第二次相等u,v间的大边覆盖小边
}
floyd();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cout<<f[i][j];//输出各个两点间的最短路
return 0;
}
用途:
1.求每两个点的最短路
附一个简单的例题,[USACO08OPEN]寻宝之路Clear And Present Danger]
求要求的路径上两相邻点的最短路的和,AC代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxm=10001;
const int maxn=101;
const int inf=100000*2;
int n,m,a[maxm],f[maxn][maxn],ans,u,v,w;
void floyd(){
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++) scanf("%d",&a[i]); a[0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{ scanf("%d",&w);f[i][j]=w;}
floyd();
for(int i=1;i<=m;i++) ans+=f[a[i-1]][a[i]];
cout<<ans;
}
2.求传递闭包
即判断两个点的联通性:f[i][j]=f[i][j]|(f[i][k]&f[k][j]);
当f[i][j]=true或者是f[i][k]=true&&f[k][j]=true时,f[i][j]=true,即联通。
例题 P2419 [USACO08JAN]牛大赛Cow Contest
求每个点以其他点的联通性,如果都联通或间接联通,这个点可以确定排名。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxm=4501;
const int maxn=101;
int n,m,f[maxn][maxn],ans,u,v,cnt[maxn];
void floyd(){
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=f[i][j]|(f[i][k]&f[k][j]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&u,&v);f[u][v]=1;
}
floyd();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(f[i][j]) cnt[i]++,cnt[j]++;//记录联通量
for(int i=1;i<=n;i++)
if(cnt[i]==n-1) ans++;//联通量到n-1时,与其他所有点的关系都有,可确定
cout<<ans;
}
3.实时加入某些点,实时求加入这些点后的两点间的最短路
核心代码:
void floyd() {
for(int k = 1; k <= n; k++) { //可以根据题目所说的顺序转移
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
f[i][j] = min(f[i][k] + f[k][j], f[i][j]);
}
}
//此处输出经过k个点后的最短路
}
}
例题
从cf上截的图:
Taunt:
首先将删点改为加点
设新加入的点为x,则我们枚举u,v(都是从1到n),然后更新
a[u][v] = min(a[u][v], a[u][x] + a[x][v]);
这样一来只要计算答案时,枚举的u,v都是当前已经加入的点,就能保证计算的所有最短路经过的点都是当前存在的点。
#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
const int maxn = 600;
int n;
ll a[maxn][maxn],p[maxn],sum[maxn];
int main() {
cin>>n;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
cin>>a[i][j];
}
}
for(int i = 1; i <= n; i++) cin>>p[n - i + 1];
for(int k = 1; k <= n; k++) {
ll ans = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
a[p[i]][p[j]]=min(a[p[i]][p[k]]+a[p[k]][p[j]], a[p[i]][p[j]]);
if(i <= k && j <= k) ans+=a[p[i]][p[j]];
}
}
sum[n-k + 1] = ans;
}
for(int i =1; i <= n; i++) cout<<sum[i]<<' ';
return 0;
}
4.找最小环,并输出路径
Taunt:
求图中的最小环。先考虑无向图。
Floyd算法保证了最外层循环到k的时候所有点对之间的最短路只经过1∼k−1号节点。
环至少有3个节点,设编号最大的为x,与之直接相连的两个节点为u和v。
环的长度应为f[u][v][x-1]+w[v][x]+w[x][u]。其中w为边权,如不存在边则为无穷大。
我们只要在进行第x次迭代之前枚举所有编号小于k的点对更新答案即可。
int ans = INF;
for(int k = 1; k <= n; k++) {
for(int i = 1; i < k; i++) {
for(int j = i+1; j < k; j++) {
ans = min(ans, f[i][j] + a[j][k] + a[k][i]);
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++) {
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
}
}
5.统计路径数
P2047 社交网络
思路:先用floyd ,如果通过k松弛的边与原先ij间最短路相等。则sum[i][j]+=sum[i][k]*sum[k][j];
如果松弛成功则需要重现计数当前松弛下来的最短路sum[i][j]=sum[i][k]*sum[k][j];
最后用一个p[maxn]数组,跑一边floyd,记录每次k是否是松弛成功的,用一遍变动的ans记录一下,最后除一下总最短路数sum[i][j]加到p[k]中。
代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=201;
typedef long long ll;
int mp[maxn][maxn],n,m;
ll sum[maxn][maxn];
double p[maxn],ans;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
mp[i][j]= 1<<29;
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
mp[u][v]=w; mp[v][u]=w;
sum[u][v]=1; sum[v][u]=1;
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(mp[i][j]==mp[i][k]+mp[k][j])
sum[i][j]+=sum[i][k]*sum[k][j];
else if(mp[i][j]>mp[i][k]+mp[k][j])
mp[i][j]=mp[i][k]+mp[k][j], sum[i][j]=sum[i][k]*sum[k][j];
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(i!=k&&i!=j&&j!=k){
ans=0;
if(mp[i][j]==mp[i][k]+mp[k][j])
ans=sum[i][k]*sum[k][j];
p[k]+=(double) ans/sum[i][j];
}
}
printf("%.3lf\n",p[k]);
}
return 0;
}
6.输出i到j的最短路的路径
添加一个矩阵p,p[i][j]表示i到j的最短行径中的j之前的第1个点
void floyd() {
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
p[i][j] = j;
}
}
for(int k = 1; k <= n; k++) {
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
if(f[i][k] + f[k][j] < f[i][j]) {
f[i][j] = f[i][k] + f[k][j];
p[i][j] = p[i][k];
}
}
}
}
//打印路径
for(int i = 1; i <= n; i++) {
for(int j = i + 1; j <= n; j++) {
int t = i;
while(t != j) {
cout<<t<<"->";
t = p[t][j];
}
cout<<j<<endl;
}
}
}
SPFA及其相关
模板:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=1e4+1;
const int maxm=5*1e5+1;
const int inf=2147483647;
int n,m,s,head[maxn],tot,vis[maxn],dis[maxn];
int read(){
int t=1,x=0; char ch=getchar();
while(ch>'9'||ch<'0'){if(t=='-') t=-1; ch=getchar();}
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
return t*x;
}
struct edge{
int u,v,w,next;
}e[maxm];
void add(int x,int y,int z){
e[++tot]=(edge){x,y,z,head[x]};
head[x]=tot;
}
void spfa(int x){
for(int i=1;i<=n;i++) dis[i]=inf;
vis[x]=1; dis[x]=0;
queue <int> q;
q.push(x);
while(!q.empty()){
int k=q.front(); q.pop();
vis[k]=0;
for(int i=head[k];i;i=e[i].next){//i=head[k] 不是 i=head[x]
int to=e[i].v;
if(dis[to]>dis[k]+e[i].w){
dis[to]=dis[k]+e[i].w;
if(!vis[to]) vis[to]=1,q.push(to);
}
}
}
}
int main()
{
n=read(); m=read(); s=read();
for(int i=1;i<=m;i++){
int a=read(),b=read(),c=read();
add(a,b,c);
}
spfa(s);
for(int i=1;i<=n;i++) printf("%d ",dis[i]);
return 0;
}
判断负环:
bool spfa(int x) {
vis[x] = 1;
for(int i = head[x]; i; i = e[i].next) {
int v = e[i].v;
if(d[v] > d[x] + e[i].w;) {
d[v] = d[x] + e[i].w;
if(!vis[v]) {
if(spfa(v)) return 1;
}
else return 1;
}
}
vis[x] = 0;
return 0;
}
记录前驱,打印最短路径:
while(!q.empty()) {
int k = q.front();
q.pop();
vis[k] = 0;
for(int i = head[k]; i; i = e[i].next) {
int v = e[i].v;
if(d[v] > d[k] + e[i].w) {
d[v] = d[k] + e[i].w;
f[v] = k; //hear
if(!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
//找父亲 输出u——>v的路径 如果想正输可以建个队列存起来
do {
cout<<v<<' ';
v = f[v];
}while(v != u);
cout<<u<<' ';
最短路计数:
在spfa过程中加以记录就好:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=1000000+1;
const int maxm=2000000+1;
const int mod=100003;
int n,m,head[maxn],tot,vis[maxn],dis[maxn],cnt[maxn];
int read(){
int t=1,x=0; char ch=getchar();
while(ch>'9'||ch<'0'){if(t=='-') t=-1; ch=getchar();}
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
return t*x;
}
struct edge{
int u,v,next;
}e[maxm];
void add(int x,int y){
e[++tot]=(edge){x,y,head[x]};
head[x]=tot;
}
void spfa(int x){
for(int i=1;i<=n;i++) dis[i]= 1<<29;
vis[x]=1; dis[x]=0; cnt[x]=1;
queue <int> q;
q.push(x);
while(!q.empty()){
int k=q.front(); q.pop();
vis[k]=0;
for(int i=head[k];i;i=e[i].next){//i=head[k] 不是 i=head[x]
int to=e[i].v;
if(dis[to]>dis[k]+1){
dis[to]=dis[k]+1;
cnt[to]=cnt[k];
if(!vis[to]) vis[to]=1,q.push(to);
}
else if(dis[to]==dis[k]+1) cnt[to]=(cnt[to]+cnt[k])%mod;
}
}
}
int main()
{
n=read(); m=read();
for(int i=1;i<=m;i++){
int a=read(),b=read();
add(a,b); add(b,a);
}
spfa(1);
for(int i=1;i<=n;i++) printf("%d\n",cnt[i]%mod);
return 0;
}
最小生成树及其相关
最小生成树基于贪心的原理,用并查集来判断,一个含有n个点的图树种,有n-1条边,那么用sort对各边升序排序,然后从外面(非当前所找图中)添加n-1边即是最小生成树,其中用并查集操作来判断添加的边(点)是否已联通即是不是已找的联通块中的,如果不是,才添加。
模板:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=5001;
const int maxm=200001;
int n,m,f[maxn],ans,sum,flag;
struct edge{
int u,v,w;
}e[maxm];
bool cmp(edge a,edge b){ return a.w<b.w;};
int find(int x){
return f[x]==x ? x :f[x]=find(f[x]);//f[x]==x 少打一个"=",wa声一片
}
bool get_find(int x,int y){
int get1=find(x),get2=find(y);
if(get1!=get2){
f[get1]=get2; return true;
}
return false ;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++) scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
sort(e+1,e+1+m,cmp);
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=m;i++){
if(get_find(e[i].u,e[i].v)){
sum++; ans+=e[i].w;
}
if(sum==n-1) {
flag=1; break;
}
}
if(flag) printf("%d",ans);
else printf("orz");
return 0;
}
其相关问题:
蓝书P343.
图片:
次小生成树
新年趣事之游戏
思路:先找最小生成树,把生成的树存起来,(注意要用两个结构体!!!这里卡了我很久)再dfs枚举树上每两个点之间的最大边权s[u][v]。
然后就是跑for枚举了,枚举加入每条不在树上的边(vis[i]=0的),加入一条边后,在树上会形成一个环(画图看看就知道),删去加入边所连接的两点间的最大边权s[e[i].u][e[i].v],用加入的边来连接图,这样生成的树就是次小生成树。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=501;
const int maxm=250000+1;
int n,m,f[maxn],tot,cnt,sum,ans1,ans2,vis[maxm],now,s[maxn][maxn],head[maxn];
int read(){
int t=1,x=0; char ch=getchar();
while(ch>'9'||ch<'0'){ if(ch=='-') t=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
return t*x;
}
struct edge{ int u,v,w,next;}e[maxm];
struct node{ int u,v,w;}bok[maxm];
bool cmp(node a,node b){return a.w<b.w;};
void add(int x,int y,int z){
e[++cnt]= (edge){x,y,z,head[x]}; head[x]=cnt;
}
int find(int x){ return f[x]==x ? x : f[x]=find(f[x]);}
bool get_find(int x,int y){
int t1=find(x),t2=find(y);
if(t1!=t2){
f[t1]=t2;
return true;
}
return false;
}
void kruskal(){
for(int i=1;i<=n;i++) f[i]=i;
sort(bok+1,bok+1+m,cmp);//最重要的贪心排序掉了?
for(int i=1;i<=m;i++){
if(get_find(bok[i].u,bok[i].v)){//判断两点的联通
tot++; ans1+=bok[i].w; vis[i]=1;
add(bok[i].u,bok[i].v,bok[i].w); add(bok[i].v,bok[i].u,bok[i].w);
}
if(tot==n-1) break;
}
}
void dfs(int x,int fa,int num){
s[now][x]=num;
for(int i=head[x];i;i=e[i].next)
if(e[i].v!=fa) dfs(e[i].v,x,max(num,e[i].w));
}
void second(){
for(int i=1;i<=n;i++)
now=i, dfs(i,0,0);
ans2= 1<<29;
for(int i=1;i<=m;i++)
if(!vis[i]) ans2=min(ans2,ans1+bok[i].w-s[bok[i].u][bok[i].v]);
}
int main(){
n=read(); m=read();
for(int i=1;i<=m;i++)
bok[i].u=read(),bok[i].v=read(),bok[i].w=read();
kruskal();
printf("Cost: ");
if(tot==n-1) printf("%d\n",ans1);
else printf("-1");
second();
printf("Cost: ");
if(ans2== 1<<29) printf("-1");
else printf("%d",ans2);
return 0;
}
Tarjan及其相关
树及其相关
树的遍历,简单的dfs:
void dfs(int now)
{
deep[now]=deep[fa[now]]+1;
sum[now]=value[now]; maxx[now]=value[now];
for 遍历从now出发的每一条边,边到达的点是v
if (v != fa[now])
{
fa[v]=now;
dfs(v);
sum[now]+=sum[v]; maxx[now]=max(maxx[now], maxx[v]);
}
}
LCA:
LCA指最近公共祖先,即同是两个点祖先的点中,深度最深的点。
直接上模板:
解释很详细qwq
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=500000+3;
int n,m,s;//点个数,询问次数,根节点编号
int head[maxn],f[maxn][20],tot,deep[maxn];
int read(){
int t=1,x=0; char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')t=-1; ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
return t*x;
}
struct edge{int u,v,next;}e[maxn<<1];
void add(int x,int y){ e[++tot]=(edge){x,y,head[x]}; head[x]=tot;}
void build(int p){
for(int i=head[p];i;i=e[i].next){//以此为根往下遍历
int to=e[i].v;
if(deep[to]==0){//深度为0即为未处理过
deep[to]=deep[p]+1; f[to][0]=p;//to的深度是其父节点p+1,即to跳2^0步到p
build(to);//继续遍历建树
}
}
}
void init_jump(){
for(int i=1;i<=19;i++)//刚打过数据,小心卡常数
for(int j=1;j<=n;j++) f[j][i]=f[f[j][i-1]][i-1];//以2的倍增方式跳,第j个点向上跳i次能达到的点就是其跳i-1次到达的点再跳i-1次
}
int lca(int x,int y){
if(deep[x]<deep[y]) swap(x,y);//如果深度x小,交换,让x为深点以便下一步处理 注意深度大的是在下面
for(int i=19;i>=0;i--)
if(deep[f[x][i]]>=deep[y]) x=f[x][i];//先处理让他们深度相等,如果跳2^i次不超过y,就跳。
if(x==y) return x;//特判
for(int i=19;i>=0;i--) //然后一起跳
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i]; //如果他们跳完之后祖先不相等的话,就跳 ,注意这里是判点而不是深度
return f[x][0];//按这样跳下去,一等会跳到只要再跳一步就能到lca的位置
}
int main(){
n=read(); m=read(); s=read();
for(int i=1;i<n;i++){//注意n-1条边
int x=read(),y=read();
add(x,y); add(y,x);
}
deep[s]=1; build(s); init_jump();//根节点深度为1,然后建树,然后初始化向上跳
for(int i=1;i<=m;i++){
int x=read(),y=read();
printf("%d\n",lca(x,y));
}
return 0;
}
用处?
我们之前说了,树上任意两点的路径都是唯一的。并且这条路径一定经过lca。
这样遇到和路径有关的问题(比如查询x到y路径的长度,查询x到y路径的节点权值最小值等等)就可以拆分成两部分:x到lca和y到lca。
对于拆分开的每一条路径的答案,我们可以用倍增数组维护,
举例:维护最小值
minn[i][j]表示从i开始往上蹦2^j,经过的所有点的权值最小值。
minn[i][0]=min(value[i],value[fa[i]])
minn[i][j]=min(minn[i][j-1],minn[fa[i][j-1][j-1])
查询的时候,在往上蹦的过程中,顺便维护一下答案。
树的前缀和
首先知道数列的前缀和: sum[i]表示a[1]~a[i]的和。
用处1:求i~j的和sum[j]-sum[i-1]
用处2:区间修改。设置一个change数组。当区间[i,j]上要加k时,我们令change[j]+=k,令change[i+1]-=k。如果我们对change数组求前缀和的话,前缀和sum_change[i]就是i这个位置变动的值
树的前缀和有两种
根路径前缀和sum2[i],指i到根节点所有节点的权值之和。
子树前缀和sum1[i],指i的子树(包括i本身)所有节点的权值之和。
这两种前缀和预处理都非常简单
树的前缀和用处
根路径前缀和,可以用来求路径节点权值和(配合lca食用)
假如要求x到y路径的权值和,x,y的lca是z。则可以用sum[x]+sum[y]-2sum[z]+value[z]
子树前缀和,可以用来做路径修改(也得配合lca食用)
设定一个修改数组change。如果要对x到y路径上的所有点权值+k,lca为z。那么change[x]+=k,change[y]+=k,change[z]-=k,change[fa[z]]-=k。这样如果最后对change[i]求前缀和的话,最后得到的结果就是i权值的修改量
特点:可以O(1)修改,但是只能一次查询(因为要求前缀和O(n))