虽然课后习题没写,但是这本书的数据结构部分总算是看完了。接下来开始算法部分。
首先简单的介绍贪婪算法:贪婪算法就是算法的每一步都是上一步条件下的最好选择。
比如梯度下降法就是一种贪婪算法,我们每次选出当前点梯度下降最快的方向作为我们下一步的方向。这种方法虽然能帮我们得到问题的解但却不是最优解,比如梯度下降法的到的路径就不是最短路径。
所以贪婪算法得到的是一个近似最优解。
下面将实现几个贪婪算法的例子。
1.货箱装载
问题:有一艘大船要装载货物,货物要装在货箱中,所有货物的大小都一样,但是货物的重量各不相同。设第i个货物的重量为wi(1<=i<=n)货物的最大载重量为c,我们的目的是在货船上装载最多的货物。
贪婪法求解准则:从剩余的货箱中,选择重量最小的货箱。
这样能保证货箱总重量最小,从而装载最多的货箱。
代码:
void containerLoading(container* c,int capacity,int numberOfContainers,int *x)
{//货箱装载贪婪算法
//container为结构体,变量为货物id及货物weight
//capacity为船的总载货量,numberOfContainers为货物的总量,x为装载上船的货物记录数组非0表示已装载
heapSort(c,numberOfContainers);//堆排序法,之前写过的一个排序法(非递减排序)
int n=numberOfContainers;
for(int i=1;i<=n;i++)//初始所有货物都没有装载上船
x[i]=0;
for(int i=1;i<=n&&c[i].weight<=capacity;i++)
{
x[c[i].id]=1;//表示货物i以装上船
capacity-=c[i].weight;//船的剩余容量
}
}
2.杂货店比赛
问题:杂货店有一场比赛,第一名将获得一些免费的商品,炸货店有n种商品。比赛规则规定,从每种商品中只能取一件。手推车的容量为c,商品i的体积为wi,价值为pi。你的目标是,装入手推车的商品的总价值最大。
可能的贪婪策略:
价值贪婪准则:从剩余的物品中选取可以装入背包的价值最大的物品。
重量贪婪准则:从剩余的物品中选出可装入背包的重量最小的物品。
价值密度pi/wi贪婪法则:从剩余物品中选出可装入包的pi/wi值最大的物品。
事实证明上述三种方法都不能适应任何情况,但都能在某些情况下找到不错的解,所以在选择贪婪目标时,我们可以根据不同情况选取不同的贪婪目标。
贪婪算法最好虽然能够获取非常接近最优解的可行解,但是最坏的情况却没有明确的上界,也就是说错误率有时候能够达到100%以上。
这时就有人提出了k阶优化。
K阶优化:首先将最多K件物品放入推车。如果这k件商品重量大于c,则放弃他,否则根据背包剩余的容量,考虑将剩余物品按pi/wi的递减顺序装入背包。
K阶优化能够保证解的结果与最优解的差在x%(x<100)之内。
例如:n=4,w=[2,4,6,7],p=[6,10,12,13,],c=11,商品id分别为1,2,3,4。
二阶优化就是首先将四种商品的任意符合条件的两种放入背包,这种选择共有6种,符合条件的有5种,[1,2],[1,3],[1,4],[2,3],[2,4]。
在2阶优化中我们得将这五种情况的其中之一放入背包然后实行贪婪算法求出解,然后再将剩余四种情况之一放入背包在实行贪婪算法,知道所有情况计算完后选取最后的那个结果。
K阶优化虽然能取得更好的结果却增加了算法的复杂度使得运算速度降低。
拓扑排序:
拓扑序列:对任务有向图的任意一条边(i,j),在这个序列中,任务i一定出现在任务j的前面。具有这种性质的序列称为拓扑序列。根据任务有向图建立拓扑序列的过程称为拓扑排序。
贪婪准则:从剩余的顶点中选择一个顶点w,它没有这样的入边(v,w),其中顶点v不在序列中。
说明:也就是说w的入边vw中的v一定在序列中,那么我们就能保证序列中的前一点一定比后一点在有向图中早出现或者没有先后关系。
引理:如果有向图有环路,那么其一定不存在拓扑排序。
所以算法如果失败就是有向图中有环路。
代码如下:
//拓扑排序
bool topologicalOrder(int *theOrder)
{//返回false,当且有向图没有拓扑序列
//如果存在一个拓扑序列,将其赋给theOrder[0:n-1]
int n=numberOfVertices();
//计算入度
int* inDegree=new int[n+1];//第0号位置不存放数据。
fill(inDegree+1,inDegree+n+1,0)//将入度数组初始化为0
for(int i=1;i<=n;i++)//循环所有的节点,每一个邻接于该节点的点的入度加1.
{
vertexIterator<T>* ii=iterator(i);//有向图的迭代器,之前我们写过,这里直接用了
int u;
while((u=ii->next())!=0)//u邻接于i。
//访问i的一个邻接点
inDegree[u]++;
}
//把入边数为0的顶点加入栈
arrayStack<int> stack;
for(int i=1;i<=n;i++)//所有入度为0的点都能成为拓扑序列的第一个点,我们将其压入栈作为候选
if(inDegree[i]==0)
stack.push(i);
//生成拓扑序列
int j=0;//数组theOrder的索引
while(!stack.empty())
{//从栈中取出顶点
int nextVertex=stack.top();
stack.pop();
theOrder[j++]=nextVertex;//先赋值在++
//更新入边数
vertexIterator<T>* iNextVertex=iterator(nextVertex);
int u;
while((u=iNextVertex->next())!=0)//如果点nextVertex加入了theOrder那么该点邻接至的顶点的入度都要减一。
{//访问nextVertex的一个邻接顶点
inDegree[u]--;
if(inDegree[u]==0)
stack.push(u);
}
}
return (j==n);//如果所有的点都加入了拓扑序列数组那么证明该图存在拓扑序列。
}
上述代码的原理很简单,首先求出所有顶点的入度,入度为0说明该点没有入点存在于有向图中,所以可以作为序列元素的候选(因为入度为0可能不止一个点)然后将候选者插入栈中,然后取出栈中的一点作为序列元素并从栈中删除这个点,再然后计算去除这个点的剩余有向图的入度,在重复之前的算法,当栈中元素为0时,拓扑排序完成。
二分覆盖
大家都知道偶图的顶点分A,B两个点集,相同点集之间的顶点之间没有连线,而不同点集的顶点之间有很多连线,比如A中的一点
a1
a
1
,可能链接B中的
b1
b
1
b2
b
2
b3
b
3
等点,二分覆盖就是想求,A中最少能有几个点能够将B中所有点都进行过连接。
二分覆盖类似集合覆盖,只是A中的点变成了一个集合,B中的点变成了集合中的元素比如
a1
a
1
={
b1
b
1
,
b2
b
2
,
b3
b
3
}.
贪婪准则:每次从A中选取一个点,它最大数量的覆盖了B中还未被覆盖的元素。
以下为算法(书上只给了伪代码):
{
coverSize=0;//当前覆盖的大小
对于A的所有i,newVerticesCovered[i]=degree[i]//计算A的每个点的入度即其覆盖数
对于B的所有i,covered[i]=false//初始时刻B中所有点并没有被覆盖
while(对于A的某些i,newVerticesCovered[i]>0)
{
设v是newVerticesCovered[i]值最大的顶点//选出入度最大的那个A中的顶点V
theCover[coverSize++]=v;//将其放入覆盖数组中作为一个覆盖顶点
for(所有邻接于v的点j)//将与V邻接的所有未被覆盖的B中的顶点标志位覆盖,并且将与其邻接的A中的顶点的入度减1
{
if(!covered[j])
{
covered[j]=true;
对于所有邻接于j的顶点,使其newVerticesCovered[k]减1
}
}
}
if(有些顶点未被覆盖)
失败
else
找到一个覆盖
}
可以证明:算法找不到覆盖,当且仅当初始的二分图没有覆盖,但是该方法可能找不到二分图的最小覆盖。
单源最短路径:
单源最短路径就是求在加权有向图中起点到达终点的最短路径。
贪婪原则:从一条路径还没有到达的顶点中,选择一个可以产生最短路径的目的顶点。
实际上下述算法将计算所有其他点到达原点的最短路径,首先求最近的点,然后通过近点逐渐求较远的点的离原点的距离。
//最短路径程序
//这个程序的逻辑有点难,我也是一步步的跟着走才勉强弄懂
void shortestPath(int sourceVertex,T* distanceFromSource,int *predecessor)
{//寻找从源sourceVertex开始的最短路径
//在数组distanceFromSource中返回最短路径
//在数组predecessor中返回顶点在路径上的前驱的information
if(sourceVertex<1||sourceVertex>n)
throw illegalParameterValue("Invalid source vertex");
graphChain<int> newReachableVertices;
//初始化
for(int i=1;i<=n;i++)
{
distanceFromSource[i]=a[sourceVertex][i];//a为有向图的成本邻接矩阵
if(distanceFromSource[i]==noEdge)
predecessor[i]=-1;
else
{
predecessor[i]=sourceVertex;
newReachableVertices.insert(0,i);
}
distanceFromSource[sourceVertex]=0;//在上一步初始化时distanceFromSpurce[sourceVertex]被初始化为noEdge,这里将其更正
predecessor[sourceVertex]=0;//同上进行更正
//更新distanceFromSource和predecessor
//之前是初始化,下面是核心代码
while(!newReachableVertices.empty())
{//还存在更多的路径
//寻找distanceFromSource值最小的,还未到达的顶点v
chain<int>::iterator iNewReachableVertices=newReachableVertices.begin();
chian<int>::iterator theEnd=newReachableVertices.end();
int v=*iNewReachableVertices;
iNewReachableVertices++;
while(iNewReachableVertices!=theEnd)
{
int w=*iNewReachableVertices;
iNewReachableVertices++;
if(distanceFromSource[w]<distanceFromSource[v])
v=w;
}
//下一条最短路径是到达顶点v
//从newReachableVertices删除顶点v,然后更新distanceFromSource
newReachableVertices.eraseElement(v);
for(int j=1;j<=n;j++)
{
if(a[v][j]!=noEdge&&(predecessor[j]==-1||distanceFromSource[j]>distanceFromSource[v]+a[v][j]))
distanceFromSource[j]=distanceFromSource[v]+a[v][j];
//把顶点j加到newReachableVertices
if(predecessor[j]==-1)
//以前未到达
newReachableVertices.insert(0,j);//如果不存在则插入,否则不插入。
predecessor[j]=v;
}
}
}
}
我们将与原点有边的点的长度全部存在distanceFromSource里面,没有边的长度也存在里面记为
noEdge。
我们将与原点相邻的点全部存在newReachableVertices里面,不相邻的不存在里面。
我们首先从newReachableVertices里面删除离原点距离最小的点,并更新连接于该点的点离原点的距离。
更新方式是短的距离替换大的距离,这样我们就能保证更新点的距离是离原点最短的距离。
如果更新点之前与原点没有边,则将其加入newReachableVertices。
我们之所以删除之前那个点,是因为它没用了,它的作用已经被利用完了。
然后再在newReachableVertices里面寻找距原点距离最短的那个点,在对其邻接点进行更新,最后我们
就能使所有点离原点的距离都是最近的。
最小成本生成树:
就是求一个连通图的最小生成树:
下面实现Kruskal算法:
//最小生成树Kruskal算法
bool Kruskal(weightedEdge<T> *spanningTreeEdges)
{//kruskal方法寻找一颗最小成本生成树
//返回false,当且仅当加权无向图是不连通的
//算法结束时,spanningTreeEdges[0:n-2]存储的是最小成本生成树的边
int n=numberOfVertices();
int e=numberOfEdges();
//建立一个数组存储边。
weightedEdge<T> *edge=new weightedEdge<T>[e+1];
int k=0;//数组edge的索引
for(int i=1;i<=n;i++)
{//取所有关联至i的边
vertexIterator<T>* ii=iterator(i);
int j;
T w;
while ((j=ii->next(w))!=0)
if(i<j)//向数组中加一条边,这个条件避免重复加入
edge[++k]=weightedEdge<int>(i,j,w);
}
//把边插入小根堆
minHeap<weightedEdge<T>>heap(1);
heap.initialize(edge,e);
fastUnionFind uf(n);//union/find 结构
//按照权的递增顺序提取边,然后决定选入或舍弃
k=0;//用于索引
while(e>0&&k<n-1)//只会有n-1条边会被选中
{//生成树没有完成且还有边存在
weightedEdge<T> x=heap.top();
heap.pop();
e--;
int a=uf.find(x.vertex1());
int b=uf.find(x.vertex2());
if(a!=b)
{//选取边x
spanningTreeEdges[k++]=x;
uf.unite(a,b);
}
}
return (k==n-1);
}
唯一需要说明的就是怎么判断加入某条边后图是否会形成环路。
刚开始加入的边a不会形成回路,然后将a边的两个点unit,然后再将b边加入,这时也不会形成回路
若a和b是邻接的那么就将边ab的点unit,如果不邻接则b的两个点unit另外形成一个类
在加入边c,若边c的两个点都存在同一个类中,那么边c加入后将会形成环路。否则加入c。