Category Archives: 算法学习

网络最大流和最小费用流

这段时间复习了下网络流模型,感觉比以前的理解有了长足进展,虽然我知道这东西难就难在建模上,而它的算法本身其实难度不大,但我还是决定说一些我的理解,毕竟理解了本质的东西运用起来才会更灵活。

最大流的求解一般有两类算法(用费用流附带求出的不列入考虑范围),就是增广路(FF)系列和预流推进(PF)系列。在很多地方都看到推荐使用后者,因为效率更高,但其实不然,今天我这篇文章就是要重点介绍前者,相信大家看了过后也会喜欢它的。

首先,那些一大堆的关于网络的定义我就不说了,不明白的就先去看看书好了,我们约定S是源,T是汇,G表示残余图。增广路算法其实相当好理解,因为只要在G中存在一条从ST的通路,那沿这条路走,流量一定还可以增加,所以当求得最大流的时候一定不存在S-T通路。 那反过来成立么?显然,因为我们有最小割最大流定理,所以由此就得到了FF算法的基本方法:

不断的在G中找一条S-T通路,然后沿其增广,直到无法找到。

至于用什么方法去找一条路,那就随便了,BFSDFS,甚至A*,你可以把你能想到的方法试个遍,看看哪个合胃口就用哪个吧。

注意那个用BFS的方法,它的官方名字叫Edmonds-KarpEK),它和我们后面要说的费用流有很大的关系,这里先简单提示一下。

那这方法的复杂度呢?如果是整数流(事实上分数流的话可以构造数据让FF无法停止,这时候必须用到PF的方法),显然每次至少增广1的流量,每次查找O(E)条边,增广要O(V)的时间,因此复杂度为O((m+n)*U)U是最大流。

听上去很恐怖,其实EK算法的一个稍微精确的上界是O(VE^2),还是很夸张。

那么,怎样才能跑得更快呢?想想,每次用BFS求出的Shortest Path TreeSPT),我们只关心了S-T最短路,其实还有很多信息没有利用到,那么如果求一次SPT,我们可以增广多次的话,是不是会好些??

于是,引入层次图的概念,说通俗点,就是求GSPT,把G中每个顶点的距离S的标号d给求出来,保留所有的s->td[s]+1=d[t]的边(注意可以不是SPT中的边),最后再在层次图中用多次BFS把所有的增广路求出来增广。

这样看似更快了,其实没有,因为分析复杂度依然是O(VE^2),没有改进,而且我们还要写更多的代码。那瓶颈在哪里呢?对了,就是那步在层次图中用BFS来找增广路。其实更好的方法是用DFS,一次就可以把所有的增广路求出来,具体做法如下:

S开始做DFS,一旦发现一条增广路,于是就沿其增广,然后DFS回退到离S最近的一条满流的边的起点处,并在图中删除该边的终点,继续搜索。

上面的算法就是著名的Dinic算法,它的复杂度为O(EV^2)(注意,看清楚2的位置),对于密图有不小的改进。

事实上,我非常推荐在各类比赛中用Dinic算法,因为它实现简单,而且实际运行速度快,比起PF算法好写太多了!!

那单纯的说好还是不行,要拿出依据来。关于PF算法的理论在本文中就不详细叙述了,可能以后我会专门介绍它。一般的PF实现是O(V^4)的,而Relable-To-FrontO(V^3),高标推进据称是O(V^2*sqrt(E)),虽然它实际中确实比较快,但是PF的算法都有一个缺点,就是必须加启发优化,至少是Gap优化,否则实际中会比较慢。因此,写一个PF的代码自然就比较多了。

上面基本上把最大流的算法盘点了一下,下面还是说一下最小费用流问题。

如果一个网络的边不仅有容量,还有单位流量费用的话,那我们自然想在求得最大流的同时,使总费用更低。因此,最小费用流实际上是一个更通用的模型,因而其应用也更广。

令人惊奇的是,解决该问题可以不需要先求出最大流来(当然也有方法需要),所以如果你不想记那么多算法的话只知道该算法也没问题。因为费用流的算法如果说复杂的话可以把线性规划扯近来,所以先罗列下几个基本方法,然后介绍最简单的一种,其它的如果你有兴趣可以自己找文章来看:

1、 连续最短路算法(Successive Shortest Path);

2、 消圈算法(Cycle Canceling);

3、 原始对偶算法(Primal Dual);

4、 网络单纯形(Network Simplex)。

后面的两个分别是前面两个的高级优化版本,其基本的优化思路就是和我们上面论述的优化增广类似,就是希望每次都能做几次运算,不要一次用了就丢弃了。最好用的就是第一个方法,方法二在Algorithm in C图论部分有详细论述,我就不说了。

还记得刚才我说的EK么?其实如果我们用DijkstraG中找S-T最短路,而不是BFS的话,是不是就解决问题了呢?是的,完全正确,我把证明留给大家想,其实很简单。

但是,有个问题,在G中存在负权的边,用Dijkstra是不是不行了呢?非要用Bellman-Ford么?其实不然,只要原图中没有负环存在(有的话不存在最小费用),那么我们可以利用Johnson算法的重赋权技术把所有的边先变成正权,然后以后每次增广后再维护一下不就可以用高效的Dijkstra了?算法如下:

Bellman-Ford求各点到S的高度标号d[]

以后每求一次最短路,设标号为pi[],那么执行:

For i=1 to v do

d[v]+=pi[v]

这样一来,标号就一直合法了(具体证明还是留给大家吧)。

好了,说这么多理论,下篇文章将会说几个具体的题目,加深对算法的理解。

开始学习计算几何

计算几何目前是我唯一几乎无法下手的题目,实在弱到自己都看不下去了,所以决定学一些东西打基础。

由于这快内容比较复杂,算法也比较难,因此准备学点东西写点文字,今天就先来看看关于凸包的内容。

说凸包首先要说凸性的定义,简单点说就是平面邻域中任意两点所在的线段上的点都在该邻域中,则该邻域具有凸性。简单推敲一下,就可以发现如果邻域中存在一阶导数不连续的点一定无法被某点集线性表示出来。再往下的内容属于数学分析了,对我们的算法设计帮助不大,暂时先不管。

一般的计算几何问题都是处理的离散点集形成的平面域,所以我们感兴趣的是怎样找一个包含这个点集的面积最小的凸多边形,这就是凸包。作为常识也应该知道凸包上的顶点必然是该点集的子集,所以根据此性质我们就可以设计高效算法。

下面将介绍三种求平面点集凸包的方法,要特别注意求多边形的凸包和平面点集的凸包的联系,这样才有助于深入学习。

Gift wrapping method:这招我想还是不说了,因为我觉得应该不会有人会用,除了比较好理解外没什么好处。

Graham-Scan:这应该是最早的O(nlgn)算法了,实现也比较简单,其基本思想是维护一个凸曲线,因此它要求算法开始时必须至少知道一个必然在凸包上的点作为其始点(还好这比较简单)。它有个缺点就是直接用它去求一个给定多边形的凸包可能会导致错误(算法艺术上给了样例),因此算法开始前必须将点有序化。

Melkman:迄今为止最好的凸包算法了,我强烈推荐的算法。它的基本操作和Graham-Scan一样,只不过它在任意时候都求得当前已考察点所形成的凸包,所以它有一个无可比拟的优势就是它是一个在线算法(要想往点集中增加一个点不必重新计算)。对于给定一个多边形,它可以直接求其凸包而不用先有序化。而它还有个最大的好处是实现非常简单,所以特别适合比赛中使用。

上面说到有序化,那什么是有序化呢?

当然,如果输入就是一个多边形,直观上讲它比一堆点似乎要有序一点。那为什么直接在一堆点上求凸包是不可行的呢?随便举个例子,想想一堆点的分布是全部在圆周上,并且圆心处有一个点,而假如你从圆心处开始算法,你觉得能得到正确答案么?从这个例子我们得到启发,我们直接在一堆点中乱转时实际上迷失了方向感,而且根据所有判凸多边形使用的基本方法-向量差积-可以知道,绕行的方向和差积的值有关,所以对一堆点,如果用直线连接最左和最右两个对踵点将多边形分成上下部分,我们至少应该知道哪些点分别可能属于上下段,这时我们才有可能利用向量差积(因为我们知道了正负分别代表什么)判凸。

解释清楚了,那怎么使其有序呢?一般有两种方法,一种是固定一点,然后求其它点相对这点的极角,再按极角排序;还有一种是水平序,即从左到右,从上到下。我推荐使用后者,因为它不会造成精度问题并且实现更简单。

以上我说得很简略,因为我觉得算法艺术这章写得非常清楚了,实在没什么需要补充的。最后还是附一个今天刚AC的POJ1113的代码,里面包含 Melkman的实现,不知道怎么写的可以参考下:

[code:cpp]

// Solution by OUSAA
// By CG

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>

#define N 1024
#define pi 3.141592653589793
#define MAKE_VECTOR(p1,p2) (struct node){P[p2].x-P[p1].x,P[p2].y-P[p1].y}
struct node
{
int x,y;
} P[N];

int H[N],np;
int D[N*2],top,bot;
int n,L;

void input()
{
int i;

for (i=0;i<n;++i) scanf(“%d %d”,&P[i].x,&P[i].y);
}

/* v1 and v2 is two vector */
inline int crossP(struct node v1,struct node v2)
{
return v1.x*v2.y-v2.x*v1.y;
}

int isleft(int p1,int p2,int p3)
{
return crossP(MAKE_VECTOR(p1,p2),MAKE_VECTOR(p2,p3));
}

/* compute convex hull via melkman algorithm */
void convex_hull()
{
int i,t;

bot=n-1;top=n;
D[top++]=0;D[top++]=1;
for (i=2;i<n;++i) {
if (isleft(D[top-2],D[top-1],i)!=0) break;
D[top-1]=i;
}
D[bot–]=i;D[top++]=i;
if (isleft(D[n],D[n+1],D[n+2])<0) {
t=D[n];
D[n]=D[n+1];
D[n+1]=t;
}

for (++i;i<n;++i) {
if (isleft(D[bot+1],D[bot+2],i)>0
&& isleft(D[top-2],D[top-1],i)>0) continue;
while (isleft(D[top-2],D[top-1],i)<=0) –top;
D[top++]=i;
while (isleft(D[bot+1],D[bot+2],i)<=0) ++bot;
D[bot–]=i;
}

for (i=bot+1;i<top;++i) H[i-bot-1]=D[i];
np=top-bot-1;
}

double length(int p1,int p2)
{
double dx,dy;

dx=P[p2].x-P[p1].x;
dy=P[p2].y-P[p1].y;
return sqrt(dx*dx+dy*dy);
}

void solve()
{
int i;
double ans,per,cosalpha,pro;

ans=0;
for (i=0;i<np-1;++i) {
per=length(H[i],H[i+1]);
ans+=per;
}

ans+=2*pi*L;
printf(“%.0lfn”,nearbyint(ans));
}

int main()
{
while (scanf(“%d %d”,&n,&L)==2) {
input();
convex_hull();
solve();
}
return 0;
}

[/code]

最后附一篇文章,里面讨论了凸包发展的历史,感兴趣的还是可以看看。convex.pdf

[zz] 小球称重问题的总结

今天在SOJ做题时发现这么个问题,比赛时推了很久公式都未果,最后忍无可忍google一下,结果一大堆现成文章。现在太晚了,先把转载过来,明天自己研究清楚了再来补充。

1.问题的提出
关于称小球问题,一般是这样的:
有N个小球外形无区别,但是有一个在质量上与其他的球不一样。用天平称最少m次一定将
不同的球找出来。显然随N增大,m不会减小。现在想解决的问题是对于任何给定的次数m,
找出在该次数下能解决的最大的N值,用Nmax来表示。并给出对应于(Nmax,m)的一种解法。
2.关于所能解决的上限
现在来求m次所能解决的上限Nmax(m)问题。
为解决这个问题,我们给出几个引理。

引理一:无论加上什么其他的附加条件,只要k个球中的任一个都有可能是坏球(概率不
为0),则当k>3^L时,称L次是称不出来的。这里的附加条件包括已知坏球是否重于好球,
除k个未知球外还提供若干个标准球,以及k个球中某些的质量和大于另外一些的和等等,
只要在这些条件下k个球中的任一个都还有可能是坏球就可以是引理所说的附加条件。
证明:很显然,若k>3^L,则哪个球是坏球一共有k中情况,而称L次一共有3^L种情况,
由k>3^L知不可能一定分辨出哪个球是坏球。

引理二:如果另外在提供任意多个标准球(即在N个未知球外还任给标准球作”砝码”用)?nbsp;
则称m次最多能从N1max(m)=(3^m+1)/2个球中找出坏球来。
证明:对该引理的证明可以采用数学归纳法。
当m=1时,显然若只有两个球,则任挑一个与另外的标准球比较(额外提供的,不是
两个中的),若相等则是剩下那一个,若不等则是这一个。所以N1max(1)>=2。
而对于三个球的情况,如果第一次称用了两个或三个未知球,则无法判断出用
过的球中谁是坏球(只称一次),而如果第一次称只用了一个未知球,则剩下
的两个球无法区分。因此一次不能解决三个球的问题。所以N1max(1)<3。
由N1max(1)<3和N1max(1)>=2知,N1max=2。
设当m<=k-1时命题都成立,则考虑m=k的情况。
第一次称不能使用超过3^(k-1)个未知球,否则如果坏球在这超过3^(k-1)个
球中的话,由引理一,在剩下的(k-1)次中不能肯定找出这个坏球来?nbsp;
另外,若第一次称碰到的都是好球,则第一次称后的结果就是多提供了一些
标准球(这个结果对已经提供了任意个标准球的情况是毫无意义的)和缩小
坏球的范围到剩下的球中。由归纳假设,剩下的球的数目不超过N1max(k-1)
才能保证一定能称出来。所以:N1max(k)<=3^(k-1)+N1max(k-1)=(3^k+1)/2。
如果有(3^k+1)/2个未知球,则第一次将3^(k-1)个未知球和提供的3^(k-1)个
未知球比较:如果相等,则坏球在剩下的N1max(k-1)个中,由归纳假设能分
出来;如果不等,则坏球在这3^(k-1)个中,但是同时知道了坏球是轻还是
重,由三分法可以很容易用k-1次找出来。所以对于(3^k+1)/2个未知球的情
况,是能够用k次找出坏球来的。即N1max(k)>=(3^k+1)/2.
由前面的推导知,N1max(k)=(3^k+1)/2。所以m=k时命题也成立。
由数学归纳法,所以N1max(k)=(3^k+1)/2对所有的自然数k都成立。
引理二得证。

引理三:Nmax(m)<=(3^m-1)/2。(m>=2)
证明:在原来的称小球问题中,起初没有提供标准球,所以第一次称的数目必须是偶数,
由和引理二中推导N1max(m)时类似,有如下结论:
Nmax(m)<=N1max(m-1) + [3^(m-1)-1]
^^^^^^^^^^ ^^^^^^^^^^^
若第一次称平衡了最多剩下的球数 第一次称最多使用的球数,必须是偶数
所以Nmax(m)<=(3^m-1)/2=N1max(m)-1。命题得证。

到此为止,我们求出了称小球问题的一个上界Nmax(m)<=(3^m-1)/2。(m>=2)
在后面我们将证明这是一个上确界,即Nmax(m)=(3^m-1)/2。
对于m=1的情况,由于必须有两个以上球(否则无所谓好坏球),所以一次是怎么也称不
出来的,因此我们不讨论m=1的情况。
3.m次称出(3^m-1)/2个球的解法
对于N(m)=(3^m-1)/2个小球,现在我们来寻求m次的解法。
首先,对于m=2的情况,相当于四个小球来称两次的情况,这个已经讨论过多次了,也很
简单,在此略去?nbsp;
其次,若m?lt;=k-1时,假定对于N(k-1)=(3^(k-1)-1)/2个球的情况我们都有解法。
现在来考虑m=k的情况。
第一次称取[3^(k-1)-1]个球放在天平天平两端,则:
如果平衡,获得[3^(k-1)-1]个标准球,坏球在剩下的[3^(k-1)+1]/2个中。由于
[3^(k-1)-1]>=[3^(k-1)+1]/2,(k>=2),即已知的标准球数不小于未知球数?nbsp;
所以在以后的测量中就相当于任意给定标准球的情况,由前面的引理二可知
对于[3^(k-1)+1]/2的情况(k-1)次可解。
如果不平衡,大的那方记做A,小的那方记作B。标准球记做C.
则现在我们有[3^(k-1)-1]/2个A球和B球,有[3^(k-1)+1]/2个C球。
第二次用3^(k-2)个A球加[3^(k-2)-1]/2个B球放左边?nbsp;
3^(k-2)个C球加[3^(k-2)-1]/2个A球放右边。
如果左边大于右边,则说明是在左边的3^(k-2)个A球中质量大的为坏球;
如果左边等于右边,则说明是在第二次称时没用的3^(k-2)个B球中质量轻
的为坏球。以上两种情况都可以再用三分法(k-2)次解决,加上前两
次共k次解决。
如果左边小于右边,则坏球在左边的[3^(k-2)-1]/2个B球中或在右边的同样
数目的A球中。此时的情况和第二次开始时类似(只不过是k-1变成k-2).
用相同的办法一直往下追溯到一个A球和一个B球一次区分的情况,这时
只需拿A球和标准球比较以下就行了。
因此在这种情况下也是可以最终用k次解决的。
由以上两步加上数学归纳法知,对于N(m)=(3^m-1)/2的情况,称m次是可以称出来的。

由这个解法加上前面所给出的上界Nmax(m)<=(3^m-1)/2,知称m次能解决的最大的小球数
Nmax(m)=(3^m-1)/2。
有兴趣的人可以验证一下m=3,N=13的情况—-该情况已经被反复拿出来讨论过了。
4.如果要求小球轻重必须判决
与前面的不需判轻重的情况类似的定义和推导,可得:
Nmax(k)=(3^k-3)/2.
N1max(k)=(3^k-1)/2.
意外的发现只比前面不分轻重的情况分别小一!!!
证明过程由于有太多类似,故在此略去,仅举一个例子来说明。
k=3,Nmax=12
共12个球
第一次: 4A ? 4B 剩下4C
如果相等,则A和B都是标准球。
第二次 C1+A ? C2+C3
如果相等,则坏球为C3,再用C3与标准球比较一次即可。
如果不等,再比较C2和C3,可以这两次结果判断C1,C2,C3中哪个是坏球和轻重。
如果不等,不妨设4A>4B.此时C球都是标准球。
第二次 A1+B2+B3+B4 ? B1+C1+C2+C3
如果相等,则坏球在A2,A3,A4中,且重。再比较A2和A3即知。
如果左边小于右边,则在B2,B3,B4中且轻。再比较B2和B3即可。
如果左边大于右边,则是A1且重或是B1且轻 ,再拿A1和标准球即可。
5.关于称12个球的明确答案
1-12
1234 vs 5678
if = then in 9-12
9,10 vs 11,1
if = then 12
if < then 9,10 轻or 11重
if > then 9,10 重or 12轻
此二者解法同下
if != 不妨假设>
125 vs 346
if = then in 78,且是轻的,7 vs 1 ->ok
if > then 12 重或者6轻
1 vs 2
if > then 1
if < then 2
if = then 6
if < then 34重或者5轻
3 vs 4
if > then 3
if < then 4
if = then 5
end
by Turbobingo@163.com From BBS 水木清华站

还找到一篇文章,非常精彩并且思考方法和独特,贴上来。smallball.doc