存储结构
//图的二维数组邻接矩阵存储 int n,e,w; //定点数和边数 权值 int g[101][101]; void make1(){ cin>>n; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j=+) g[i][j]=0x7fffffff; } //初始化 cin>>e; int x,y; for(int i=1;i<=e;i++) { cin>>x>>y>>w; g[x][y]=w; g[y][x]=w; //这是由于无向图,所以有两句 } } //邻接表 struct node{ int from,to,dis; node(int a,int b,int c){ from=a;to=b;dis=c; } }; vector<node> adj[maxn]; //数组模拟邻接表 //专业名------链式前向星 struct Edge{ int next; //下一条编的编号 int to; //这条边的去处 int dis; //边的权值 }edge[1001]; //这是边 int head[101],num_edge; //这是节点 void add_edge(int from,int to,int dis){ //添加一条从from到to的距离为dis的单向边! edge[num_edge].to=to; edge[num_edge].dis=dis; edge[++num_edge].next=head[from]; head[from]=num_edge; } void make2(){ cin>>n>>e; num_edge=0; int x,y,d; for(int i=1;i<=e;i++){ cin>>x>>y>>d; add_edge(x,y,d); } for(int i=head[1];i!=0;i=edge[i].next){ //遍历操作 } } int main(){ return 0; }
图的遍历
BFS、DFS,通过图的遍历也可以找到连通块的个数
重要概念:
深度优先生成树
回退边
void dfss(int x,int depht){ //带层数的(邻接表版) vis[x]=1; for(int i=1;i<=adj[x].size();i++){ int j=adj[x][i]; if(vis[j]) dfs(j,depth+1); //不需要判断是不是连接 } } //广度优先 struct node{ int v; //顶点编号 int layer; //层号 }; vector<node> ajd[MAXN]; void bfsss(int x,int layer){ node start; start.v=x; //起始编号 start.layer=0; queue<node> q; q.push(start); vis[x]=1; while(!q.empty()){ node now=q.front(); q.pop(); int u=now.v; for(int i=1;i<ajd[u].size();i++){ node j=adj[u][i]; //相连的顶点 j.layer=now.layer+1; if(vis[j.v]==0){ q.push(j); vis[j.v]=1; } } } } //对整张图进行遍历 //如果图是联通的,那么只需要进行一次遍历了 void travdfs(){ for(int i=1;i<=n;i++){ if(vis[i]==0) dfs(i); } } void travbfs(){ for(int i=1;i<=n;i++){ if(vis[i]==0) bfs(i,1); //层数 }
欧拉路(一笔画问题)、欧拉回路
从图中某个点出发遍历整个图,每条边通过且通过一次。
一、是否存在欧拉路或欧拉回路
(1)图应该是联通图:DFS或并查集
(2)无向图:全部都是偶点:存在欧拉回路
有两个奇点:存在欧拉路,一个为起点,一个为终点
(3)有向图:每个点的出度标记为1,入度标记为-1,出度+入度即为度数,
有向图存在欧拉路:只有1个度为1(起点),1个度为-1(终点),其他都为0
有向图存在欧拉回路:全部都为0
二、输出欧拉回路:
递归DFS,在后面打印或记录,但是如果数据很大,就得采用非递归形式。
三、混合图欧拉回路问题:最大流
http://ybt.ssoier.cn:8088/problem_show.php?pid=1341
int n,e; int circuit[101],d[101][101],c[101]; //c是每个点的度,用来判断是欧拉路还是欧拉回路 int num; void find(int i){ for(int j=1;j<=n;j++){ if(d[i][j]==1){ d[i][j]=d[j][i]=0; //删除这条边 find(j); } } circuit[++num]=i; //记录路径 } //这是欧拉路(2个奇点),欧拉回路(0个奇点) int main(){ cin>>n>>e; int x,y; memset(c,0,sizeof(c)); memset(d,0,sizeof(d)); for(int i=1;i<=e;i++){ cin>>x>>y; d[x][y]=d[y][x]=1; c[x]++; c[y]++; } int start=1; for(int i=1;i<=n;i++){ if(c[i]%2==1) start=i; //从奇点开始 } num=0; find(start); for(int i=1;i<=num;i++) cout<<circuit[i]<<" "; cout<<endl; return 0; }
哈密尔顿环:不重复的走过所有的点,并且是回路
//哈密尔顿环:不重复的走过所有的点,并且是回路 //能找出所有的环 int vi[1001],visted[1001],num[1001],g[1001][1001]; int length; int ans[1001]; //保存答案 int n,m,x; void print(){ for(int i=1;i<length;i++) cout<<ans[i]<<" "; cout<<ans[length]<<endl; } void dfs(int last,int i){ //上次访问的last,这次的i visted[i]=1; vi[i]=1; //标记 for(int j=1;j<=num[i];j++){ if(g[i][j]==x&&g[i][j]!=last){ ans[++length]=j; print(); //找到一个环,输出 length--; break; } if(!visted[g[i][j]]) dfs(i,g[i][j]); //遍历所有与i关联的点 } length--; visted[i]=0; //回溯 不标记vi[],因为vi表示是否在图中出现过 } int main(){ memset(visted,0,sizeof(visted)); memset(vi,0,sizeof(vi)); cin>>n>>m; int y; for(int i=1;i<=m;i++){ cin>>x>>y; g[x][++num[x]]=y; g[y][++num[y]]=x; } for(x=1;x<=n;x++){ if(!vi[x]) { length=0; dfs(0,x); } //以每一个点为起点遍历,因为不是任一个点都可以遍历出环的 } return 0; }
最短路径
dijkstra:O(N^2),,单源最短路径,不能有负边.可以通过堆优化为O(nlogn+m)
//图的结构都用邻接表写 //第一种:最简单的加上记录路径 struct node{ int v; int dis; }; vector<node> G[maxn]; int pre[manx]; //最简单的一种pre写法(苦笑—— //输出过程 int dis[maxn]={0}; //记录起点与其他各点的最短距离 void outputdfs(int v,int st){ if(v==st){ cout<<st<<endl; return; } outputdfs(pre[v],st); cout<<v<<" "; } void dijkstra1(int st){ int numnode,numedge,x,y,diss; cin>>numnode>>numedge; for(int i=0;i<numedge;i++){ cin>>x>>y>>diss; node a,b; a.v=x;a.dis=diss;b.v=y;b.dis=diss; G[x].push_back(b); G[y].push_back(a); //无向图 }//以后可以直接写构造函数 fill(dis,dis+maxn,INF); dis[st]=0; // for(int i=0;i<numnode;i++) pre[i]=i; //初始化pre数组不要忘了!!!! for(int i=0;i<numnode;i++){ int u=-1,numi=INF; for(int j=0;j<numnode;j++){ if(vis[j]==0&&dis[j]<mini){ mini=dis[j]; u=j; } } //找点的过程 if(u==-1) return; //退出标志 vis[u]=1; //这是第一阶段 for(int j=0;j<G[u].size();j++){ //更新与找到的这个点相连的点 int v=G[u][j].v; if(vis[v]==0&&dis[v]<dis[u]+G[u][j].dis){ dis[v]=dis[u]+G[u][j].dis; pre[v]=u; } } } }
有第二标尺的
边权标尺(花费等,至于边权!=距离)
int cost[maxn][maxn]; int c[maxn]; /* struct node{ int v; int dis; }; vector<node> G[maxn]; int dis[maxn]={0}; */ void dijkstra2(int st){ fill(dis,dis+maxn,INF); dis[st]=0; fill(c,c+maxn,INF); c[st]=0; for(int i=0;i<numnode;i++){ int u=-1,mini=INF; for(int j=0;j<numnode;j++){ if(vis[j]==0&&dis[j]<mini){ mini=dis[j];u=j; } } if(u==-1) return ; vis[u]=1; //以上为第一阶段 for(int j=0;j<G[u].size();j++){ int v=G[u][j].v; if(vis[v]==0){ if(dis[v]>dis[u]+G[u][j].dis){ dis[v]=dis[u]+G[u][j].dis; c[v]=c[u]+cost[u][v]; } else if(dis[v]==dis[u]+G[u][j].dis&&c[v]>c[u]+cost[u][v]){ c[v]=c[u]+cost[u][v]; //因为是花费,所以越小越好 } } } } }
点权(例如资源等)越多越好
int weight[maxn]; int w[maxn]; /* struct node{ int v; int dis; }; vector<node> G[maxn]; int dis[maxn]={0}; */ void dijkstra3(int st){ fill(w,w+maxn,0); //注意不同:点权的其他不等于起点的都赋值位0,边权赋值位无穷大 w[st]=weight[st]; fill(dis,dis+maxn,INF); dis[st]=0; //初始化阶段 for(int i=0;i<numnode;i++){ int u=-1,mini=INF; for(int j=0;j<numnode;j++){ if(vis[j]==0&&dis[j]<mini){ mini=dis[j];u=j; } } if(u==-1) return ; vis[u]=1; //以上为第一阶段 for(int j=0;j<G[u].size();j++){ int v=G[u][j].v; if(vis[v]==0){ if(dis[v]>dis[u]+G[u][j].dis){ dis[v]=dis[u]+G[u][j].dis; w[v]=w[u]+weight[v]; } else if(dis[v]==dis[u]+G[u][j].dis&&w[v]<w[u]+weight[v]){ w[v]=w[u]+weight[v] } } } } }
路径条数
int num[maxn]; //就多这一个数组 /* struct node{ int v; int dis; }; vector<node> G[maxn]; int dis[maxn]={0}; */ void dijkstra4(int st){ fill(num,num+maxn,0); //与点权一样:与起点不同的都赋值为0;起点为1 num[st]=1; fiil(dis,dis+maxn,INF); dis[st]=0; for(int i=0;i<numnode;i++){ int u=-1,mini=INF; for(int j=0;j<numnode;j++){ if(vis[j]==0&&mini>dis[j]){ mini=dis[j];u=j; } } if(u==-1) return ; vis[u]=1; for(int j=0;j<G[u].size();j++){ int v=G[u][j].v; if(vis[v]==0){ if(dis[v]>dis[u]+G[u][j].dis){ dis[v]=dis[u]+G[u][j].dis; num[v]=num[u]; //继承 } else if(dis[v]==dis[u]+G[u][j].dis){ num[v]+=num[u]; //加上 } } } } }
第二标尺不满足最优子结构时,需要改变算法,即不能在Dijkstra的算法过程中直接求出最优而是应该先求出所有的最优路径,然后选择第二标尺最优的那条路。所以采用Dijkstra+DFS的方法,Dijkstra求出所有的最优路径,DFS求出第二标尺最优的
所以改变是pre[maxn]---vector<int> pre[maxn]‘
vector<int> pre[maxn]; void dijkstra5(int st){ fill(dis,dis+maxn,INF); dis[st]=0; for(int i=0;i<numnode;i++){ int u=-1,mini=INF; for(int j=0;j<numnode;j++){ if(vis[j]==0&&mini>dis[j]){ mini=dis[j];u=j; } } if(u==-1) return ; vis[u]=1; //以上为第一阶段 //改变的是下面的第二阶段,在记录最优路径的时候 for(int j=0;j<G[u].size();j++){ int v=G[u][j].v; if(dis[v]>dis[u]+G[u][j].dis){ dis[v]=dis[u]+G[u][j].dis; pre[v].clear(); //先清空 pre[v].push_back(u); } else if(dis[v]==dis[u]+G[u][j].dis){ //如果距离一样 ,就压入 pre[v].push_back(u); } } } } //接下来找出第二标尺最优的那个路径 //当画出这个路径时,会发现是一颗树的结构,根节点是终点,叶子节点都是起点(所以在有些情况下需要逆序),这样走下来找到最优标尺 //因为有多条路径,每次决定走哪条,所以用递归搜索+回溯的方法 //有一点绝对要注意,因为最后的叶子节点(起点)无法自己入数组,所以需要自己碰到叶子节点是把它push进来 vector<int> temppath,path; //一个用来临时存路径,一个用来存最优路径 int maxvalue; void DFS(int st,int v){ if(v==st){ temppath.push_back(v); //计算这条路径上的最优路径值 int value=0; //eg:边权值和 for(int i=temppath.size();i>0;i--){ //这两个例子其实都满足最优子结构,可以直接用dijkstra来解,但是这个通用模板必须记住 //计算边权值和,边界时i>0; int now=temppath[i],next=temppath[i-1]; value+=G[now][next].dis; } //eg:点权值和 for(int i=temppath.size();i>=0;i--){ //计算点权值和:边界为i>=0 int id=temppath[i]; value+=weight[id]; } if(value>maxvalue){ maxvalue=value; path=temppath; } //记录最优路径 //不要忘记弹出噢!!! temppath.pop_back(); return; //以及return噢!~ } temppath.push_back(v); for(int i=0;i<pre[v].size();i++){ DFS(st,pre[v][i]); } temppath.pop_back(); //也不要忘记弹出回溯噢!!!~ }
Bellman-ford:O(NM),
对边进行遍历。不能有负权回路,但是能提示,可用循环队列。
但是如果从原点无法到达负环的话,是不会有有影响的。
可以处理负边权,再进行以此松弛操作既可以判断是不是存在负环 、所有的边进行操作,看能不能通过这条边来进行优化
最短路径树:层数不超过V,源点s作为根节点,其他节点按照最短路径的节点顺序连接
注意求路径数的时候,vector<int> pre[maxn]要改为set<int> pre[maxn]
bool ford(int s){ fill(dis,dis+maxn,INF); dis[s]=0; //n是节点个数,因为最后是一棵树,所以边数为n-1即一共只需要n-1次循环 for(int i=0;i<n-1;i++){ for(int j=0;j<n;j++){ for(int z=0;z<adj[j].size();z++){ int v=adj[j][z].v; int di=adj[j][z].dis; if(di+dis[j]<dis[v]) dis[v]=dis[j]+di; } } } //再遍历以下所有的边,看还能不能松弛 for(int i=0;i<n;i++){ for(int j=0;j<adj[i].size();j++){ int v=adj[i][j].v; int di=adj[i][j].dis; if(dis[v]>dis[i]+di) return 0; } } return 1; } //如果用ford算法求解路径的话 //需要用 set<int> pre[maxn]; int num[maxn]; if(dis[v]>dis[j]+di){ dis[v]=dis[j]+di; num[v]=num[j]; //直接覆盖 pre[v].clear(); pre[v].insert(j); } else if(dis[v]==dis[j]+di){ pre[v].insert(j); //先插入 num[v]=0; //先付0 for(set<int>::iterator it=pre[v].begin();it!=pre[v].end();it++) num[v]+=num[*it]; //不是直接加*it啊 }
SPFA:ford的队列实现,单源最短路径,与BFS的区别:出了队的可以再次入队。
对ford的优化:只有最短路改变了的才可能继续改变其他的节点的最短路,所以没必要访问全部
判断有无负环的方法是计算每个节点的入队次数,如果入队次数超过n就存在负环了
int vis[maxn];//这是用来记录是不是在队列里面的 int num[maxn]; //记录入队次数(如果说明不存在负环就不需要这个) bool spfa(int s){ fill(dis,dis+maxn,INF); dis[s]=0; vis[s]=1; num[s]++; //入队次数+1 queue<int> q; q.push(s); while(!q.empty()){ int top=q.front(); q.pop(); vis[top]=0; //出队了 //接着访问这个节点的所有邻接边 for(int i=0;i<adj[top].size();i++){ int v=adj[top][i].v; int diss=adj[top][i].dis; if(dis[v]>dis[top]+diss) { dis[v]=dis[top]+diss; //先松弛,然后判断能不能入队 if(!vis[v]){ q.push(v); vis[v]=1; num[v]++; if(num[v]>=n) return 0; } } } } return 1; }
Floyd:O(N^3),全源最短,可以处理负边权,可以判断负环
负环判断:初始化所有的dp[i][i]=0后,如果结束时dp[i][i]<0,那么就存在负环
//n在200以内 //在main()函数里面先执行: for(int i=0;i<n;i++) dis[i][i]=0; //然后是函数体 void floyd(){ for(int k=0;k<n;k++){ for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ if(dis[i][k]!=INF&&dis[k][j]!=INF&&dis[i][k]+dis[k][j]<dis[i][j]) dis[i][j]=dis[i][k]+dis[k][j]; } } } }
SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。个人觉得LLL优化每次要求平均值,不太好,为了简单,我们可以之间用c++STL里面的优先队列来进行SLF优化。
最小生成树:在无向图中,连接所有的点,不形成环,使所有的边权值和最小的树
算法有:prim算法: dijkstra算法类似(dis[i]的含义不同,dijkstra中是起点,prim中是已经访问过的所有点) 稠密图时使用 O(V^2)
kruskal算法:并查集思想,每次都找到最小的边,判断这两个边连接的点是不是在同一个集合中,如果不是就连接起来 稀疏图时使用 O(ElogE)
struct node{ int v,dis; node(int _v,int _dis) : v(_v),dis(_dis){} }; vector<node> adj[maxn]; int dis[maxn],vis[maxn]; int n,m,st,ed; void prim(int st){ //树的总权值,以及当前所有的连接好了的边 for(int i=0;i<n;i++){ int u=-1,mini=INF; //与dijkstra是不是超级像!!!!! for(int j=0;j<n;j++){ if(mini>dis[j]&&vis[j]==0) { mini=dis[j]; u=j; } } if(u==-1) return; vis[u]=1; ans+=dis[u]; //!!!!!啊啊啊记住这个 for(int j=0;j<adj[u].size();j++){ int id=adj[u][j].v; int diss=adj[u][j].dis; if(!vis[id]&&dis[id]>diss){ dis[id]=diss;//!!!! } } } }
kruskal
struct edge{ int from,to; int dis; }E[maxn]; int fa[maxn]; int findfather(int x){ if(x!=fa[x]) return findfather(fa[x]); return fa[x]; } bool cmp(edge a,edge b){ return a.dis<b.dis; } void kruskal(int n,int m){ //n是顶点数,m是边数 for(int i=0;i<n;i++) fa[i]=i; //先弄这个 fill(dis,dis+maxn,INF); memset(vis,0,sizeof(vis)); dis[0]=0; int ans=0,numedge=0; //这里才有!!总的权值和现在有了的边 sort(E,E+m,cmp); //对边进行排序 for(int i=0;i<m;i++){ int fa1=findfather(E[i].from); int fa2=findfather(E[i].to); if(fa1!=fa2){ fa[fa2]=fa1; numedge++; ans+=E[i].dis; if(numedge==n-1) break; //!!!!!!!如果边数已经够了的话就可以break了 } } if(numedge!=n-1) { cout<<"error no liantong"<<endl; return; } else{ cout<<ans<<endl; return; } }
拓扑排序:前提条件是有向无环图(DAG),排列成为有序的。通过队列、计算入度,每次把入度为0的加入队列,然后删掉所有从这个点出发的边,每个相连的点的入度-1
应用:判断图是不是有向无环图 。如果队列为空时,入过队的为n,那就排列成功,不然就有环
//下面是伪代码 //用邻接表实现 struct node{ int v,dis; }; vector<int> adj[maxn]; int innode[maxn]; //节点入度 vector<node> adj[maxn];//临界表 bool list(){ int num=0; //这个是已经有序的节点个数 queue<int> q;//队列 for(int i=0;i<n;i++){ if(innode[i]==0) q.push(i); //节点出度为0的,都压入 } while(!q.empty()){ int top=q.front(); q.pop(); for(int j=0;j<adj[top].size();j++) { int id=adj[top][j]; innode[id]--; if(innode[id]==0) q.push(id); } adj[top].clear();//删掉所有与之相邻的边 num++; } if(num==n) return 1; else return 0; }
拓扑排序用bFS和DFS都能实现
BFS:无前驱的顶点优先,无后继的顶点优先
DFS:从一个入度为0的点开始DFS,递归返回的顺序就是拓扑排序(逆序),可以用stack实现
入度为0的点:不需要特别处理,想象一个虚拟的点,单向连接到所有点,只要在主程序中把每个点轮流执行一遍DFS
判断环:递归时发现回退边
关键路径:
//首先是区分事件和活动,事件是节点,活动是边,因为是求关键活动,所以是先通过求事件的最早发生和最迟发生,然后再来求活动的最早开始和最晚开始 //先求点,再夹边 //事件: ve[i] vl[i] //活动: e[j] l[j] //求事件(顶点)最早发生和最迟发生: ve[j]=max{ve[i]+length[i->j]} (i->j) 拓扑排序 // vl[i]=min{vl[j]-length[i->j]} (i->j) 逆拓扑排序 //之间的关系是,求活动(边)的最早开始和最晚开始: e[i->j]=ve[j] (i->j) // l[i->j]=ve[j]-length[i->j] //拓扑排序序列用stack存储,这个逆拓扑排序就不用特意去求了 //求拓扑序列,顺便求ve[N] stack<int> toporder; int ve[maxn],vl[maxn]; bool logicalsort(){ //int num=0; //已经在了的点 //不用num了,直接判断toporder.size()就可以了 queue<int> q; for(int i=0;i<n;i++) if(innode[i]==0) q.push(i); //先把入度为0的点全部入队 while(!q.empty()){ int top=q.front(); q.pop(); toporder.push(top); //拓扑序列进栈 for(int i=0;i<adj[top].size();i++){ int id=adj[top][i].v; innode[id]--; //入度-1 if(innode[id]==0) q.push(id); //边是top->id if(ve[top]+adj[top][i].dis>ve[id]) ve[id]=ve[top]+adj[top][i].dis; } } if(toporder.size()==n) return 1; else return 0; } //接下来就是求关键路径了,求出vl,然后计算出e[],l[],如果e[i]==l[i] 就是关键活动 int criticalpath(){ memset(ve,0,sizeof(ve)); if(logicalsort()==0) return -1; //先把所有的vl[]都赋值为ve[n-1] ,然后进行逆拓扑序列求解 fill(vl,vl+n,ve[n-1]); while(!toporder.empty()){ int top=toporder.top(); toporder.pop(); for(int i=0;i<adj[top].size();i++){ int id=adj[top][i].v; //top的后继节点是id,用id的值来更新top if(vl[id]-adj[top][i].dis<vl[top]) vl[top]=vl[id]-adj[top][i].dis; } } //遍历临界表所有的边,计算活动的e[]和l[] for(int i=0;i<n;i++){ for(int j=0;j<adj[i].size();j++){ int v=adj[i][j].v; int diss=adj[i][j].dis; int e=ve[v],l=vl[v]-diss; if(e==l) cout<<e<<"->"<<l<<endl; } } }
动态规划实现DAG最长路
第一种:不固定起点终点
//用动态规划实现的:最简单 //不固定起点和终点 //dp[i]表示从i出发能获得的最长路:递归+记忆化(已经自动实现了字典序最小 //如果dp[i]表示以i结尾的:不能实现最小序 //记录路径:choice[]记录后继 int dp[maxn]; int g[maxn][maxn]; int chioce[maxn]; int dp(int i){ if(dp[i]>0) return dp[i]; //记忆化 for(int j=0;j<n;j++){ if(g[i][j]!=INF){ int temp=dp(j)+g[i][j]; //递归 if(temp>dp[i]){ dp[i]=temp; choice[i]=j; } } } return dp[i]; } void print(int i){ cout<<i<<" "; while(choice[i]!=-1){ //记录的就是后继 i=choice[i]; cout<<i<<" "; } }
第二种:固定终点T
与前一种的区别在于初始化,dp[]应该被初始化为-INF,表示不可达,但是dp[T]=0,另外设置一个vis[]数组
int dp(int i){ if(vis[i]) return dp[i]; vis[i]=1; for(int j=0;j<n;j++){ if(g[i][j]!=INF){ dp[i]=max(dp[i],dp(j)+g[i][j]); } } return dp[i]; }