关于图算法的总结

 例,01问题

求长度为n的01串,满足如下条件

1,长为L0的连续子串中0的个数不少A0,不多于B0

2,长为L1的连续子串中的1的个数不少于A1,不多于B1

如果不存在,输出-1

 

这个题可以构造图,然后求图的各点的最短路径。

由于上述1,2条件的存在,对于任意的k,用f(k)表示前k串的和

则f(k+L0)-f(k)>=L0-b0

   f(k+L0)-f(k)<=L0-a0

 

   f(k+L1)-f(k)>=a1

   f(k+L1)-f(k)<=b1

 

   f(k+1)-f(k)>=0

   f(k+1)-f(k)<=1

 

根据<=的关系可以构造一个图,图中的各个节点就是从1到n,关系如上不等式。相当于求n个点到点0的最短距离。而相邻的两个点与0点之间的距离就是我们要求的该位置的0或者1

 

由于该算法中权值存在负值,所以用bellman松弛法求单源最短路径

源代码如下

#include "stdafx.h" #include #include #include void buildGraph(int n,int firstL,int secondL,int a0,int a1,int b0,int b1,int *h,int **graph); void getH(int n,int *h,int ** graph); void print(int n,int *h); int main() { std::cout<<"ok"<graph[i][j]+h[i]) { std::cout<<-1<=firstL) { graph[i][i-firstL]=b0-firstL; graph[i-firstL][i]=firstL-a0; } if(i>=secondL) { graph[i-secondL][i]=b1; graph[i][i-secondL]=-a1; } if(i>1) { graph[i][i-1]=0; graph[i-1][i]=1; } } }

 

bellman_ford算法

BELLMAN_FORD(G,w,s)

 Init-single-source(G,s)

for i<-1 to |V[G]|-1

do for each edge(u,v) in E[G]

do relax(u,v,w)

for each edge(u,v) in E[G]

do if d[v]>d[u]+w(u,v)

then return FALSE

return TRUE

 

松弛法

Relax(u,v,w)

if d[v]>d[u]+w(u,v)

then d[v]<-d[u]+w(u,v)

trace[v]<-u;

 

 

 

同时复习一下Dijkstra算法

 

Dijkstra(G,w,s)

 Init-single-source(G,s)

S=null;

Q=V[G];

while Q!=null

 do u=min(Q)

 S=S+{u}

 for each vertex v in Adj[u]

do Relax (u,v,w)

附一个Dijkstra算法的实现

 

for(int i=1;i<=pointNum;i++)

{

min=MaxInt;

for(int j=i+1;j<=pointNum;j++)

{

if(!isVisited[j])

{

if(d[1][j]

{

minPoint=j;

min=d[1][j];

}

}

}

isVisited=true;

for(int j=1;j<=pointNum;j++)

{

if(d[pointNum][j]+min

d[1][j]=d[pointNum][j]+min;

}

}

 

 

2,二分图

求二分图的最大匹配有两种算法:最大流,匈牙利算法

求二分图的最佳匹配:最大流。

 

最大流的算法基于以下的思想:残留网络,增广路径,最小割。

求最大流的算法过程:1,随意找一个源点到终点的增广路径。2,求该网络的残留网络。3,在残留网络上找增广路径,将增广路径上可通过的最大值加到该路径上的图的流上。4重复2,3直到没有新的增广路径。找增广路径的时候用广度优先算法可以增加算法的效率。

 

对二分图求最大最优匹配的时候,只用额外的加一个源点和终点即可。

求最大匹配最好还是用匈牙利算法。

 

以下用匈牙利算法来求解,国王心爱的王子选妻子的题目:国王有n个儿子,宫里有n个美女。每个王子喜欢一些姑娘(艹),国王对每个儿子的喜欢程度不同为Ai,如果王子挑选到了喜欢的姑娘,国王的高兴程度+Ai^2。找一个算法,让国王足够满意。

 

关于二分图的另一个说明:完全二分图的边的数目小于n^2/4,证明很简单,省略。

在下面的程序中,王子为1~n,女孩为n+1~2n,边的数目正好为n^2,所以开了n^2条边。

 

 #include "stdafx.h" #include #include #ifndef PRINCESSELECTGIRLS_H #define PRINCESSELECTGIRLS_H struct LovedGirl { int index; int next; }; class PrincesSelectGirls { public: PrincesSelectGirls() { std::cin>>n; kingLove=new int[n+1]; princesGirls=new LovedGirl[n*n]; isVisited=new bool[n+1]; link=new int[n+1]; first=new int[n+1]; tot=0; a=new int[n+1]; memset(isVisited,false,sizeof(isVisited)*(n+1)); memset(link,0,sizeof(link)*(n+1)); memset(first,0,sizeof(first)*(n+1)); memset(a,0,sizeof(a)*(n+1)); for(int i=1;i<=n;i++) { std::cin>>kingLove[i]; a[i]=i; } int tempInt; for(int i=1;i<=n;i++) { for(int j=i+1;j<=n;j++) { if(kingLove[i]>tempInt; while(tempInt>0) { add(i,n+tempInt); std::cin>>tempInt; } } for(int i=1;i<=n;i++) { memset(isVisited,false,sizeof(isVisited)*(n+1)); find(a[i]); } } void add(int x,int y) { tot++; princesGirls[tot].index=y; princesGirls[tot].next=first[x]; first[x]=tot; } bool find(int x) { int k=first[x]; while(k!=0) { int i=princesGirls[k].index; if(!isVisited[i-n]) { isVisited[i-n]=true; if(link[i-n]==0||find(link[i-n])) { link[i-n]=x; std::cout<<"prince "<

 

关于二分图题目2

求二分图中的必须的边

 

算法有两种:1,枚举所有的边,然后用匈牙利算法求最大匹配,如果找不到最大匹配,则是必须的

       这种算法的情况下,如果是邻接矩阵的话,枚举每条边需要的时间是O(n^2),当然也可以存边,存边的话O(e),匈牙利算法的复杂度为O(EN)

 

  2,缩小查找空间,必须的边一定在一个完备匹配中,因为必须的边在所有的完备匹配中,如果少了这个边,一个完备匹配都找不到,所以可以先找一个完备匹配,然后剔除其中的边,用匈牙利算法验证。这样的话,将边的范围减小了。

 

代码如下

#ifndef NECEEDGE_H #define NECEEDGE_H #include #include #define MAXN 101 class NeceEdge { public: NeceEdge() { memset(edge,0,sizeof(edge)); memset(match,0,sizeof(match)); memset(visited,0,sizeof(visited)); memset(g,0,sizeof(g)); std::cin>>edgeNum>>n; for(int i=1;i<=edgeNum;i++) { std::cin>>edge[i][0]>>edge[i][1]; g[edge[i][0]][edge[i][1]]=1; } findNum=0; for(int i=1;i<=2*n;i++) { memset(visited,0,sizeof(visited)); if(getFirstPerfect(i)) findNum++; } if(findNum2*n) break; g[j][match[j]]=0; if(getMatch()

 

 

3, 线段树的应用

问题:树的统计。一棵含有n个节点的树,所有节点的编号依次为1,2,3,...,n。对于编号为v的节点,定义t(v)为后代中所有编号小于v的节点个数。请计算每个节点i的t(i)。

用到的数据结构,树,线段树。算法dfs

不妨先思考这样一个方法:假如这是一颗二叉树。我们将这棵二叉树保存在数组里面。从数组最末开始,向前扫描(本质是从叶子节点开始)。对于每一个节点,和父节点n/2比较,如果比这个父节点小,父节点的t值+1,然后父节点的父节点。算法复杂度为(nlogn)当然二叉树比较好的情况的时候。不好的时候复杂度就要高,最坏n^2。

对于这棵树,不方便用二叉树来做。

 

本题的算法很有意思。它是这样做的,先对树做一次正向dfs先序遍历,得到遍历序列,然后反向dfs先序,得到一个遍历序列,而,两个遍历序列重叠的部分,正好是树根的子节点。那么

t=正向dfs中小于v的节点数量+逆向中的+祖先中小于v的-整棵树小于v的(整棵树小于v的为v-1个)

 

该算法对线段树进行了灵活的应用,值得学习。一下是代码和测试用例

#include "stdafx.h" #include #include #ifndef TREECOUNT_H #define TREECOUNT_H //在该算法中运用了线段树的思想,在线段树的构造过程中,完成了对dfs顺序逆序序列的统计,该算法的时间复杂度为nlogn #define MAXM 100//用来表示节点最大的度,例如,最大为5叉的树 #define MAXN 100//最多的节点上数量 class TreeCount { public: TreeCount() { memset(dfs1,0,sizeof(dfs1)); memset(dfs2,0,sizeof(dfs2)); memset(isVisited,false,sizeof(isVisited)); memset(e,0,sizeof(e)); memset(sum,0,sizeof(sum)); std::cin>>n; for(int i=1;i<=n;i++) { std::cin>>e[i][0]; for(int j=1;j<=e[i][0];j++) { std::cin>>e[i][j]; this->isVisited[e[i][j]]=true; } } for(int i=1;i<=n;i++) if(this->isVisited[i]==false) root=i; dfsC1=0; dfsC2=0; dfs(root); dfsN(root); memset(sum,0,sizeof(sum)); memset(t,0,sizeof(t)); for(int i=n;i>=1;i--) { t[dfs1[i]]=t[dfs1[i]]+check(1,1,n,dfs1[i]-1); //t[dfs1[i]]=t[dfs1[i]]+sum[dfs1[i]-1]; insert(1,1,n,dfs1[i]); } memset(sum,0,sizeof(sum)); for(int i=n;i>=1;i--) { t[dfs2[i]]=t[dfs2[i]]+check(1,1,n,dfs2[i]-1); //t[dfs1[i]]=t[dfs1[i]]+sum[dfs1[i]-1]; insert(1,1,n,dfs2[i]); } memset(sum,0,sizeof(sum)); dfsP(root); for(int i=1;i<=n;i++) { t[i]=t[i]-i+1; std::cout<e[i][0];j++) { dfsP(e[i][j]); } this->deleteNode(1,1,n,i); } void TreeCount::deleteNode(int i,int s,int t,int x) { if(t==s) { sum[i]--; return; } int middle=(t+s)/2; if(x<=middle) deleteNode(i*2,s,middle,x); else deleteNode(i*2+1,middle+1,t,x); sum[i]=sum[i*2]+sum[i*2+1]; } void TreeCount::insert(int i, int s, int t, int x) { if(s==t) { sum[i]++; return; } int middle=(s+t)/2; if(x<=middle) { insert(2*i,s,middle,x); } else { insert(2*i+1,middle+1,t,x); } sum[i]=sum[2*i]+sum[2*i+1]; } int TreeCount::check(int i, int s, int t, int x) { if(t==s) { return sum[i]; } int middle=(s+t)/2; if(x<=middle) { return check(2*i,s,middle,x); } else { return check(2*i,s,middle,middle)+check(2*i+1,middle+1,t,x); } } void TreeCount::dfs(int i) { dfsC1++; this->dfs1[dfsC1]=i; for(int j=1;j<=e[i][0];j++) dfs(e[i][j]); } void TreeCount::dfsN(int i) { dfsC2++; this->dfs2[dfsC2]=i; for(int j=e[i][0];j>=1;j--) dfsN(e[i][j]); } #endif 

 

 

测试用例

 

15

0

0

2 12 15

0

0

2 5 8

5 1 3 4 9 10

0

2 6 11

1 14

0

0

0

2 2 13

0

 

 

运行结果0 0 0 0 0 1 6 0 3 1 0 0 0 2 0

 

4,多源点最短路径问题

 

考虑对远点进行合并,然后使用dijkstra算法,记住压缩图和原图之间点序号的映射

 

#ifndef MULTISOURCEPOINT_H #define MULTISOURCEPOINT_H #define MAXN 500 #define MAXINT 10000 #include int a[MAXN][MAXN]; int b[MAXN]; int c[MAXN]; bool isVisited[MAXN]; void mutiSourcePoint() { int pointNum,edgeNum,sourcePoint; for(int i=0;i>pointNum>>edgeNum>>sourcePoint; int temp=0; for(int i=1;i<=sourcePoint;i++) { std::cin>>temp; c[temp]=1; } b[0]=1; for(int i=1;i<=pointNum;i++) { b[i]=b[i-1]-c[i]+1; c[b[i]]=i; } int x,y,z; for(int i=1;i<=edgeNum;i++) { std::cin>>x>>y>>z; if(b[x]==1&&b[y]!=1) a[1][b[y]]=z; else if(b[x]!=1&&b[y]!=1) a[b[x]][b[y]]=z; } int t=1; int min=MAXINT; int minPoint=1; isVisited[1]=true; for(int i=1;i<=pointNum;i++) { min=MAXINT; for(int j=i+1;j<=pointNum;j++) { if(!isVisited[j]) { if(a[1][j]

 

5, 闭包运算的题目

 

如何计算闭包,实际上就是floyd算法的思想。

两种方法

1,采用宽度优先或者深度优先遍历来解决。从任意一个顶点出发,进行一次遍历,就可以求出次顶点和其他各个顶点的联通状况。所以只需要把每个顶点做为出发点遍历一次,就能够知道任意两个顶点之间是否有路存在。穷举每个顶点,总的时间复杂度是O(n*n)

2,类似floyd算法,如下算法实现的那样。

 

题目:运动员之间可以传递信息,但不是每两个运动员之间都可以相互传递信息。求教练至少要通知几个人才可以使每个运动员都知道自己的命令。

算法,先计算闭包,给每个闭包赋予一个序号。

然后按照序号将图压缩,一个传递闭包一个节点。

统计入度为0的节点的个数,即为教练需要通知的次数

#include #include #define MAXN 100 bool snet[MAXN][MAXN]; bool anet[MAXN][MAXN]; int sIndex[MAXN]; bool avgIn[MAXN]; int n,e; void TeamProblem() { memset(snet,0,sizeof(snet)); memset(anet,0,sizeof(anet)); memset(sIndex,0,sizeof(sIndex)); memset(avgIn,0,sizeof(avgIn)); std::cin>>n>>e; int a,b; for(int i=1;i<=e;i++) { std::cin>>a>>b; snet[a][b]=true; } for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { if(snet[i][j]||(snet[i][k]&&snet[k][j])) snet[i][j]=true; } int index=0; for(int i=1;i<=n;i++) { if(sIndex[i]==0) { index++; sIndex[i]=index; for(int j=1;j<=n;j++) { if(snet[i][j]&&snet[j][i]) sIndex[j]=index; } } } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) if(!anet[sIndex[i]][sIndex[j]]&&sIndex[i]!=sIndex[j]) anet[i][j]=snet[i][j]; } for(int i=1;i<=index;i++) { for(int j=1;j<=index;j++) if(anet[i][j]) avgIn[j]=true; } int total=0; for(int i=1;i<=index;i++) if(!avgIn[i]) total++; std::cout<


floyd算法又称floyd-warshall算法,是一种动态规划算法,用来求一个有向图G中每对顶点间的最短路径问题,其运行时间为O(v^3)

 

定义n*n的矩阵W。过程范围最短路径的权值矩阵为D(n)

 

Floyd-Warshall(W)

n<-rows(W)

D(0) <-W

     for(k=1~n)

         do for i=1~n

             do for k=1~n

 

             do d(k)ij =min(d(k-1)ij , d(k-1)ik +d(k-1)kj )

return D(n)

 

6, 使用floyd算法的例子。

染色

一个N个点的无向完全图,每条边都被染成[1,m]的一种颜色,要求选择一部分颜色,作为一个集合S,且S中的元素个数不超过(m+1)/2,并满足如下性质:

图中任意两个点a和b,一定存在一条长度不大于3的通路,其每条边的颜色都属于S。

简单的分析:

将m种颜色一分为2,然后证明必有一半的颜色满足。

 

使用然后,设A集合边为1,B集合的边的权重为4。使用floyd算法,求两点之间的最短距离。如果超过了3,则结果为B集合,否则为A集合。

代码如下

#ifndef DYEPROBLEM_H #define DYEPROBLEM_H class DyeProblem { int n;//节点个数 int cn;//颜色数目 int **g;//图 public: DyeProblem(); int min(int x,int y); }; #endif 

#include "stdafx.h" #include #include "DyeProblem.h" DyeProblem::DyeProblem() { std::cin>>n>>cn; g=new int*[n+1]; for(int i=1;i<=n;i++) g[i]=new int[n+1]; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { std::cin>>g[i][j]; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { if(g[i][j]<(cn+1)/2) g[i][j]=1; else g[i][j]=4; } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { for(int k=1;k<=n;k++) { g[i][j]=min(g[i][j],g[i][k]+g[k][j]); } } } bool answerIsA=true; for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { if(g[i][j]>3) answerIsA=false; } } if(answerIsA) { for(int i=1;i<=(cn+1)/2;i++) std::cout<

 7,二叉树的应用

 

坐船问题:某学校有n个学生去公园划船。一条船最多可以坐两个人。如果某两个学生同姓或者同名就可以坐同一条船。学校希望每个学生都上船,但是小船的租用费用很高。学校想要租最少的船。请问学校要租多少条船。

 

用二叉树来解决。左孩子同姓,右孩子同名。在选择将谁放在一条船上的时候有以下几种情况

1,如果父节点已经被其他节点使用了,则增加一条船。

2,如果父节点没有被使用,并且父节点的另一个节点已经被使用,则当前节点和父节点一条船

3,如果非1,2情况。若父节点是父父节点的左孩子,则父节点和右孩子被访问,左孩子连接到父父节点

4,若父节点是右孩子,则右孩子连接到父父节点

 

代码如下

#ifndef BOATPROBLEM_H #define BOATPROBLEM_H #include class BoatProblem { int n;//the num of student; int *f;//tree father nodes; int *left; int *right; bool **used; bool *isVisited; int ans; public: BoatProblem() { std::cin>>n; f=new int[n+1]; left=new int[n+1]; right=new int[n+1]; isVisited=new bool[n+1]; used=new int*[n+1]; ans=0; for(int i=0;i<=n;i++) { isVisited[i]=false; f[i]=0; left[i]=0; right[i]=0; used[i]=new int[n+1]; for(int j=0;j<=n;j++) used[i][j]=false; } int temp=0; for(int i=1;i<=n;i++) { std::cin>>temp; while(temp!=0) { if(!isVisited[temp]&&!used[i][temp]) { isVisited[temp]=true; used[i][temp]=true; used[temp][i]=true; if(f[temp]==0) { int j=i; while(left[j]!=0) { j=left[j]; } f[temp]=j; left[j]=temp; } else { int j=temp; while(left[j]!=0) { j=left[j]; } f[i]=j; left[j]=i; } } std::cin>>temp; } std::cin>>temp; while(temp!=0) { if(!isVisited[temp]&&!used[i][temp]) { isVisited[temp]=true; used[i][temp]=true; used[temp][i]=true; if(f[temp]==0) { int j=i; while(right[j]!=0) { j=right[j]; } f[temp]=j; right[j]=temp; } else { int j=temp; while(right[j]!=0) { j=right[j]; } f[i]=j; right[j]=i; } } std::cin>>temp; } } for(int i=0;i<=n;i++) { isVisited[i]=false; } for(int i=1;i<=n;i++) { if(!isVisited(i)) dfs(i); } std::cout<

你可能感兴趣的:(算法)