Introduction to algorithms 3rd 阅读总结和部分习题分析

内容目录
 1 一些网站    2
 2 Chapter 3 growth of functions    2
 3 Chapter 4 divide and conquer    3
 3.1 4.1 max sub array问题。    3
 4 sorting    7
 5 chapter 2.1 insert sort    7
 5.1 插入排序的改进shell sort    8
 6 chapter 2.3 merge sort    9
 6.1 Problems    11
 6.1.1 Problem 2-4 inversions    11
 7 bubble sort    12
 8 选择排序    12
 9 chapter 5 heap sort    13
 9.1 chapter 6.1 heap sort    13
 9.2 chapter 6.5 priority queue    14
 9.3 problems    15
 9.3.1 d-ary heap    15
 9.3.2 Young tableaus    15
 10 Chapter 7 quick sort    20
 11 linear sorting    23
 11.1 counting sort    23
 11.2 Radix sort    24
 11.2.1 Exercise    25
 12 Chapter 15 dynamic programming    26
 12.1 Chapter 15.1 Rod cutting problem    28
 12.2 Chapter 15.2 matrix-chain multiplication problem    30
 12.3 Chapter 15.4 Longest comman subsequence (LCS) problem    32
 12.3.1 练习15.4.5和15.4.6 找一个数字序列中最长的增序子数列。longest increasing subsequence (LIS) 问题, 此问题是o(nlgn)的复杂度。    33
 12.4 problems    35
 12.4.1 15-1 DAG中的最长路径问题    35
 12.4.2 拓扑排序    36
 12.4.3 Problem 15.2 Longest plindrome subsequence (LPS)    37
 12.4.4 Problem 15.3 Bitonic euclidean traveling-salesman problem    38
 12.4.5 15-4 printing neatly problem    39
 12.4.6 15-5 Edit distance problem    41
 12.4.7 15-7 viterbi algorithm    42
 12.4.8 15-8 image compression by seam carving    44
 12.4.9 15-9 breaking a string    46
 12.4.10 15-10 planing an investment strategy    48
 12.4.11 15-11 inventory planning    49
 12.4.12 15-12 Signing free-agent baseball players    50
 13 chapter 16 greedy algorithms    52
 13.1 Activity selection problem    52
 13.1.1 练习16.1.4    55
 13.2 0-1 knapsack problem和fractional knapsack problem. 0-1背包问题和能够取部分物体的背包问题    56
 14 chapter 24 single source shortest path    58
 14.1 Chapter 24. 1 Bellman-ford algorithm    58
 14.2 chapter 24.2 求DAG有向无环图的最短路径(利用拓扑排序降低复杂度)    60
 14.2.1 应用DAG的最短路径算法求最长路径,解决PERT chart的critical path问题    60
 14.2.2 如何计算DAG中,从一个点开始到另一各点的所有路径    60
 14.2.3 练习24.2-4 找一个DAG中所有的path    60
 14.3 Chapter 24.3 Dijkstra's algorithm    61
 14.3.1 练习24.3-2    62
 14.3.2 练习24.3.-6    62
 14.4 chapter 24.4 difference constraints and shortest paths    63
 14.5 练习    63
 14.5.1 Problem 24-3    63
 14.5.2 bitonic shortest path    66
 15 chapter 25 all pairs shortest path    67
 15.1 chapter 25.1    67
 15.2 Chapter 25.2 floyd-warshall 算法    68
 15.2.1 Transitive closure    70
 15.3 problems    71
 16 Chapter 26 maximum flow    71
 16.1 Chapter 26.2 the ford-fulkerson method    71

 1  一些网站
下面网站有全部算法,以及一些interview题目。
http://www.geeksforgeeks.org/fundamentals-of-algorithms/#DynamicProgramming

 2  Chapter 3 growth of functions
一个算法是Θ(n2)表明该算法忽略一些常量因子或者低级别的内容后,就是n2的级别。
一个算法是O(n2)表明该算法的最差复杂度是n2级别。可能会低于n2级别。所以O表明了upper bounds.
插入排序当最差的时候复杂度是Θ(n2), 当已经有序时,是Θ(n), 我们说他的排序算法是O(n2)
一个算法是Ω(n2),表明该算法的时间复杂高于const value * n2级别。N2是lower bounds.
我们可以说insert sort的最好复杂度是Ω(n).

 3  Chapter 4 divide and conquer
分治法。
问题分解为同样类型的子问题+不同的另外问题(如果有的话)。
采用递归描述同样的子问题,对于不同的另外问题,找到解决方式。
构建递归算法公式,从而找到递归解决方案。
 3.1  4.1 max sub array问题。
Suppose that you been offered the opportunity to invest in the Volatile Chemical
Corporation. Like the chemicals the company produces, the stock price of the
Volatile Chemical Corporation is rather volatile. You are allowed to buy one unit
of stock only one time and then sell it at a later date, buying and selling after the
close of trading for the day. To compensate for this restriction, you are allowed to
learn what the price of the stock will be in the future. Your goal is to maximize
your profit. Figure 4.1 shows the price of the stock over a 17-day period. You
may buy the stock at any one time, starting after day 0, when the price is $100
per share. Of course, you would want to “buy low, sell high”—buy at the lowest
possible price and later on sell at the highest possible price—to maximize your
profit.
只允许买卖一次,问题转换成一个数组。元素有正负。求和最大的子数组问题。
解决方案:
low, mid, high代表第一天,中间一天,以及最后一天。
问题转换成求[low, mid], [mid+1, high]中的max subarray以及跨越mid的最大子数组。
前后半个区间的最大子树组问题可以用递归表示,就是跨越mid的需要另外找方案。
它可以转化为[low,mid]中[i..mid]最大的子数组,也就是以mid结尾的最大子树组问题。
以及[mid+1,high]中以mid+1开头的最大子数组问题,这必须要遍历才能解决。

公式:
查找跨越mid的最大子数组问题:
上述负责度o(n)



总的复杂度:
最中是:O(nlgn)

另一种解法:号称线性
练习题
答案:
设f(n) 返回值{low, high, maxsum}
设p[j] 代表index j的数组值
adj[x]代表以x结尾的最大子数组,返回值[i, maxsum],其中i表示一个下标
我们知道,因为我们从头开始计算,那么:
因为adj[x]必须包含x,所以adj[x].maxsum就必然是adj[x-1].maxsum+p[x]或者p[x]本身.。可见adj的推算是常量的。如果我们允许负数,那么:

f(n) = max(f(n-1), adj[n]).
n从0开始,f(0) = {0, 0,p[0]}
f(1) = max(f(1), adj[1])

那么计算f(n)需要从0, ...n次计算,每次计算需要常量的adj计算和大小比较,所以复杂度是O(n)

adj[n] 算法如下:是O(n)
adj[0] = {0,p[0]}
I = 1;
while(i<n)  //n次循环
{
    //下面计算常量级别
    if(adj[i-1].maxsum+p[i] >p[i] )
    {
            adj[i].maxsum = adj[i-1].maxsum+p[i];
            adj[i].i = adj[i-1].i;
    } else {
        adj[i].maxsum = p[i];
        adj[i].i = i;
    }
    i++;
}

可以先计算出所有adj.然后计算fn.
f(n)算法如下:是O(n)

I = 1;
f(0) = {0,0,p[0]}
while(i<n) //n次循环
{
//下面比较常量级别。因为adj已经算出来了。
    if( f(i-1).maxsum > adj(i).maxsum )
       {
        f(i).low = f(i-1).low;
        f(i).high = f(i-1).high;
             f(i).maxsum = f(i-1).maxsum;
     } else {
        f(i).low = adj[i].i;
        f(i).high = i;
             f(i).maxsum = adj[i].maxsum;
        }
     i++;
}
可见两个O(n)级别的算法相加还是o(n).可见此算法是线性级别。比上面那个递归的要高一些。


注意,f(n), adj[n]中有负数出现的可能。
 4  sorting
排序,有基于比较的,并且已经证明基于比较的算法最好的复杂度就是o(nlgn).
一个排序算法性能列表:

 5  chapter 2.1 insert sort
o(n2)
 5.1  插入排序的改进shell sort
blog.csdn.net/morewindows/article/details/6668714
将n个元素的直接插入排序分成多份,每间隔若干元素的序列成员执行一次直接插入排序。间隔逐渐缩小成1.利用了较为有序的输入的插入排序性能较高的特点。
shell_sort(int a[], int n) {
    for(int gap = n/2; gap >0; gap /= 2 ) {
        //针对每个gap有gap组序列:
        for(i=0; i<gap; i++ ) {
            insert_sort(a, n, I, gap); //对a中元素直接插入排序,  i是其实第一个元素的位置,gap是元素位置之间的gap.
        }
    }
}

insert_sort(int a[], int n, int statrtindex, int gap ) {
    for( int I = startindex+gap; i<=n-1; i+=gap) {
        int j=1;
        int val = a[i];
        while( val<a[i-gap*j] && I-gap*j >=0) {
            a[i-gap*(j-1)] = a[i-gap*j];
            j++;
        }
        a[i-gap*(j-1)] = val;
    }
}

 6  chapter 2.3 merge sort

Merge sort是典型的分而治之的策略,divide and conquer.
复杂度:最差o(nlgn), 需要o(n)额外空间










将一串数字分成两半,分别处理两个子问题,自问题互相没有overlap, 然后将结果combine到一起。
Void mergesort( int a[], int begin, int end ) {
    if( begin >= end )
        return;
    int mid = (begin+end)/2;
    mergesort(a, begin, mid);
    mergesort(a, mid+1, end);
    merge(a, begin, mid, end);
}

void merge( int a[], int start, int mid, int end ) {
    //there are two sub arrays [start, mid] and [mid+1, end]
    //make a copy of them first.
    Int len1 = mid-start+1;
    int len2 =  end-mid;
    Int* Copy1 = malloc(len1*sizeof(int));
    int* copy2 = malloc(len2*sizeof(int));
    memcpy((void*)copy1, (void*)(a+start), len1*sizeof(int));
    memcpy((void*)copy2,(void*)(a+mid+1), len2*sizeof(int));
    int i=0;
    int j = 0;
    int k = start;
    while( i<= len1 && j<= len2 ) {
        if( copy1[i] <= copy2[j] ) {
            a[k++] = copy[i];
            i++;
        } else {
            a[k++] = copy2[j];
            j++;
        }
    }

    if( i<=len1 ) {
        memcpy((void*)(a+k),(void*)(copy1+i),(len1-i+1)*sizeof(int));
    } else if(j<=len2) {
        memcpy((void*)(a+k),(void*)(copy2+j),(len2-j+1)*sizeof(int));
    }
    delete copy1;
    delete copy2;
}

其实,merge sort也可以通过对小问题使用insert sort来提高性能。
 6.1  Problems
 6.1.1  Problem 2-4 inversions




首先就应该想到分治法。
一串数字,从中间分开成n1, n2两部分,那么inversion(n) = inversion(n1)+inversion(n2)+
n1和n2这两部分之间数据的inversion数量。

问题b: 当然是递减的序列含有最多的inversion了。
问题c: 插入排序时,比较一个元素和前面的元素,如果是inversion,就移动,看来插入排序的时间复杂度或者说移动次数就是inversion数目。那么计算的复杂度o(n2)
问题d: 类似于merge sort,把问题分开,然后merge时,把自问题的inversion 数目加起来,然后再仿照merge操作,比较子问题的元素大小,算出n1,n2两个子问题之间的Inversion 数目。 o(nlgn)
 7  bubble sort
冒泡排序,性能很差,不断比较并交换相邻两个元素的位置。
o(n2)
void bubble(int a[], int n ) {
    //perfrom n-1 loops
    for( int i=0; i<n-1; i++) {
        //swap from i till n-1
        for( int j=i; j<n-1; j++ ) {
            if( a[j] >a[j+1]) { //如果条件成了a[j]>=a[j+1]就不是稳定排序了。
                int tmp = a[j+1];
                a[j+1] = a[j];
                a[j] = temp;
            }
        }
    }
}
 8  选择排序
每次循环找到最小元素,和最终位置上的目前元素换位。o(n2), 不稳定。
select_sort(int a[], int n) {
    for( int i=0; i<n-1; i++ ) { //只需要N-1次循环    
        min = a[i]
        minindex = I;
        for( int j=i+1; j<=n-1; j++ ) { //从i元素后面的元素开始处理。
            if( a[j] < min ) {
                min = a[j];
                minindex = j;
            }
        }

        //find the minest value
        swap(i, minindex); //将最小元素放到合适的位置。这个长距离交换会破坏稳定性
    }
}
 9  chapter 5 heap sort
 9.1  chapter 6.1 heap sort
优点:in-place, O(nlgn) worst case. 该算法构造的heap结构除了可用来排序外,还可用来做其他事情。
之所以worst case下还是nlgn,因为max-heap或者min-heap保证数据永远是complete tree,这样的结构保证了树的操作总是和树高相关,而树基本总是平衡的。
很好,但是大数据量时,quick sort经常优于heap sort.

思想:构建最大heap, 每次交换堆顶元素和最后元素,然后递归调整堆。







构建最大堆的方法:bottom up方法,从第一个非叶子节点开始,逆向max-heapify节点到树根。复杂度O(n).:




max-heapify的方法,比较树根和两颗子树的根,并调整位置,然后递归调整子树。复杂度o(h) = o(lgn).











除了最大堆还有最小堆,可以用来实现priority queue. 在dijkstra算法中可以使用priority queue来找最小元素。此外,还需要不断减小剩余节点的路径值,所以在min heap的效率也较高。 性能较高。
 9.2  chapter 6.5 priority queue
max priority queue可以用来schedule jobs, 每次都取出最大优先级的job, job可以插入,删除,可以提升优先级。当然也可一降低优先级。
min priority queue可以用来表示事件的时间顺序,最小的时间就是要最先要被安排执行的事件。
priority queue涉及到的操作包括:
1. 构建,等同于构建max heap, o(n)
2. 获取最大值,直接取第一元素即可
3. 提取并移除最大元素,取出第一元素,将他和最后一个元素换位,size-1, 然后max-heapify, O(lgn)
4. 插入新元素,插入到最后,设初始值负无穷,然后将值跟新成新值,然后调用增加一个元素值的算法o(lgn)
5. 增加一个元素的值,不断比较该元素和其父亲,如果必要就交换位置,直到不再需要交换o(lgn)
6. 删除一个元素, 将该元素和最后元素交换位置,并针更新后的该元素调用max-heapify. o(lgn)


























 9.3  problems
 9.3.1  d-ary heap
d 维的heap, 简称d-heap , 比如2-heap, 3-heap, d代表孩子分支的数量。
https://en.wikipedia.org/wiki/D-ary_heap
这种树高度低,但是因为孩子多,所以每次比较操作需要和较多的孩子比较。
 9.3.2  Young tableaus
是一个m*n矩阵,每一行从左到右已经排序,每一列,从上到下,已经排序。
如果矩阵中有一个位置为正无穷,那么该矩阵不是满的。
可以观察出,该矩阵最左上角的元素是最小元素。且如果一个矩阵是满的,那么矩阵的最右下角元素是最大元素。
我们可以把此矩阵看成binary heap的一种变形,如果把binary heap的某些节点重合,就成了矩阵状。

对于c问题,extract-min的实现,因为最小元素位于最左上角,所以直接取出来,然后把最后一行最后一个元素移到左上角,然后从新adjust一下整个矩阵。这个adjust因为可看成把原有元素内容增加,所以可命名为adjust-increase.
调整方法:i,j位置元素的右元素和下元素中,我们取较小者,应该是目前真正的较小者,把他和i,j互换。比如和下面元素互换,换完后,横向的元素已经排序完成,那么检查纵向是否满足顺序,如果还不满组,那么问题转换成针对i-1,j元素调用adjust-increase递归问题。
extract-min(A,m,n)
{
    min = A[0,0];
      A[0,0] = last(A,m,n);
    adjust-increase(A,0,0,m,n), 从0,0位置开始调整。
}

//调整
adjust-increase(A,i,j,m,n)
{
    hasRight = false;
    hasDown = false;
    int SwapMode = 0;
    int newI = i;
    int newJ = j;

    right = 0;
    down = 0;
    //先判断i,j右边和下面是否有元素
    if(j+1<=n-1)
    {
        hasRight = true;
        right = A[i,j+1];
    }
    if(i+1 <= m-1)
    {
        hasDown = true;
        down = A[i+1,j];
    }
    //如果下面和右边都有元素,且A[i,j]大于其中至少一个,那么就和其中较小者互换位置。
    if( hasRight && hasDown )  {
        if ( !(A[i,j] <right && A[i,j]<down) ) {
            if(right < down)
                swapMode = SWAP_RIGHT;
            else
                swapMode = SWAP_DOWN;
        }
    } else if( hasRight ) {
        if( A[i,j]>right) //如果只有右边元素,那么如果大于右边元素,就和右边互换
            swapMode = SWAP_RIGHT;
    } else {
        if( A[i,j]>down) //如果只有下边元素,那么如果大于下边元素,就和下边互换
            swapMode = SWAP_DOWN;
    }
    
    //如果需要互换调整,就互换云素,然后调整新位置。
    //这样问题就会递归解决。直到不许要调整。
    if( swapMode ) {
        if(swapMode == SWAP_RIGHT)
            newI = i+1;
        else if(swapMode == swapDown)
            newJ = j+1;
        swap(A,i,j,newI, newJ);
        adjust-increase(A,newI,newJ,m,n)
    }
}

因为元素走过的复杂度是O(m+n),其实实际上开始可能并不需要将最后一个元素填充到左上角。我们可以采取一种挪动元素的方案。不断挪动元素,来填补空缺。
问题d:插入一个元素。
矩阵是非满的,借鉴max heap里插入元素的算法,设先将最后一个元素后面的元素插入一个值为正无穷的元素。那么他天然满足条件。然后将该元素数值减小。我们只需要实现adjust-decrease(A,i,j,m,n)算法即可,即实现减小一个元素的算法。I,j元素变小,就应该向左,或者向上走,那么如何走?如何将问题转换为小问题?
分case:
case 1: aij 依然满足大于上面和左边元素,那么不用动了。
Case 2: aij大于左边,但是小于上边元素。那么说经上边元素肯定大于左边元素。aij直接和上面元素互换,换完后,对i-1,j位置做检查调整即可。其他位置目前应该满足条件。
Case 3: aij大于上面,但是小于左边,那么左边肯定大于上面,将aij和左边互换,问题转换为检查调整i,j-1元素。
复杂度也是O(m+n)

问题e:
排序,我么可以类似heap sort,先把素有元素构建出该矩阵,然后extract-min, n此extract-min即可。构建该矩阵也可通过插入操作。
 10  Chapter 7 quick sort
期待复杂度O(nlgn),且算法常数因子较小,最差o(n2), 并且是in-place排序,不需要额外空间。
当partition效果差,分出的两部分不平衡时,导致性能变差,当输入已经有序(不管升序还是降序)时,都是最差o(n2)
思想:把元素divide and conquer
把元素partition成两部分,然后再对子问题递归完成排序。
partion是关键,可以选第一各元素或者最后元素,或者随机选元素。
下面是选第一个元素作为pivot的算法:
这个算法比较容易记住:
void sort(int *a, int left, int right)
{
    if(left >= right)/*如果左边索引大于或者等于右边的索引就代表已经整理完成一个组了*/
    {
        return ;
    }
    int i = left;
    int j = right;
    int key = a[left];
    
    while(i < j)                               /*控制在当组内寻找一遍*/
    {
        while(i < j && key <= a[j])
        /*而寻找结束的条件就是,1,找到一个小于或者大于key的数(大于或小于取决于你想升
        序还是降序)2,没有符合条件1的,并且i与j的大小没有反转*/
        {
            j--;/*向前寻找*/
        }
        
        a[i] = a[j];
        /*找到一个这样的数后就把它赋给前面的被拿走的i的值(如果第一次循环且key是
        a[left],那么就是给key)*/
        
        while(i < j && key >= a[i])
        /*这是i在当组内向前寻找,同上,不过注意与key的大小关系停止循环和上面相反,
        因为排序思想是把数往两边扔,所以左右两边的数大小与key的关系相反*/
        {
            i++;
        }
        
        a[j] = a[i];
    }
    
    a[i] = key;/*当在当组内找完一遍以后就把中间数key回归*/
    sort(a, left, i - 1);/*最后用同样的方式对分出来的左边的小组进行同上的做法*/
    sort(a, i + 1, right);/*用同样的方式对分出来的右边的小组进行同上的做法*/
                       /*当然最后可能会出现很多分左右,直到每一组的i = j 为止*/
}


另一份实现:选第一元素作为pivot:
void quick_sort(int s[], int l, int r) 

    if (l < r) 
    { 
        //Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1 
        int i = l, j = r, x = s[l]; 
        while (i < j) 
        { 
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数 
                j--;   
            if(i < j)  
                s[i++] = s[j]; 
             
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数 
                i++;   
            if(i < j)  
                s[j--] = s[i]; 
        } 
        s[i] = x; 
        quick_sort(s, l, i - 1); // 递归调用  
        quick_sort(s, i + 1, r); 
    } 

http://blog.csdn.net/morewindows/article/details/6684558

改良1: 为了尽量避免出现最差情况,挑选pivot的方式改成随机。
为了避免对算法修改,我们找到随机的元素位置后,和最后或者第一个元素先互换一下位置,证样我们可以依然沿用上面的每次都拿第一个元素作为pivot的算法。

改良2:快速排序的另一种改良(结合插入排序):
选一个k, 当子问题长度是k时,就不排了,留待快排完成后,整体走一遍插入排序。利用插入排序在大体上已经有序的情况下性能较高的特点。
快速排序在整体有序时性能较差,而插入排序在整体有序时,性能最高,是o(n),我们结合这两者能获得较好的性能。
因为长度为k的小问题不许要在处理,相当与问题规模缩小,可以认为快排复杂度是o((n/k)lg(n/k)), 对剩下的元素的插入排序认为是o(n), 那么加一起就是o(n + (n/k)lg(n/k)), 整体乘一个k得到:
o(nk+nlg(n/k))
也可以针对小于某特定长度的子采用插入排序,而不是统一对整个问题插入排序。

此外,改良1和2可以结合。

改良3:median-of-3 partition
就是partition时,随机挑选3个元素,找中间元素做pivot.

其实,之前有一个老的partition算法,是原本的快速排序的算法:
hoare partition:


 11  linear sorting
不同于前面的基于比较的排序,基于比较的排序,最好也就是o(nlgn)。
这里的排序可以达到o(n)级别,不基于比较。
比较排序的worst case的算法的最佳复杂度是o(nlgn)的证明:
decision tree. n个元素有n!个排列,decision tree中,每个排列对应一个叶子。那么到一个叶子的高度就是一个算法复杂度。也就是树高。那么树高应该是最好的worst case复杂度了。
有n! <=l <=2的h次方。l代表叶子的数目。h代表树高。
那么有n! <= 2的h次方。不等式两边取对数。
有lg(n!) <= h, 而已知lg(n!) == o(nlgn), 所以h >= o(nlgn). 也就是复杂度的极限就是o(nlgn)
 11.1  counting sort
counting sort不使用比较,他为每个元素计数,然后算出每个元素前面有多少元素,于是,就知道了自己位置了。
设n个元素,元素的取值为于0..k之间。
o(n+k), 并且是stable的排序。 可以被radix sort使用。k是元素的范围,比如在0...k之间
空间复杂度也是o(n)




A包含待排序列,B是排序结果,C包含了A中所有的元素但没有重复,比如C中有k个元素,对应0,....k-1, 他们允许重复的出现在A中,A中共有n个元素。
4-5行,计算每个元素在A中出现的次数,并计入C中。
7-8行针对c中每个元素,计算小于等于他的元素的数目,其实也就是计算出了该元素的位置。
11行为每个元素安排位置,因为元素可能重复,所以需要第12行,每次都递减一下C中的数值,这样,当遇到重复的元素时,重复元素的位置会相邻于上次的位置。
如果把第10行排序从1开始,那么就会破坏stable的特点。
为什么? 因为所谓stable,就是说,相等的元素排序后,还保持相对的位置不变,如果我们按照原程序排序,已知元素x共有3次重复,那么我们从后面向前排,遇到第一次x时,其实是最后一个x, 从C中取出的位置是最靠后的位置,然后c中位置--, 下次遇到第2个x时,会给x安排相邻的靠前的位置,一次类推,所以是稳定的,但是一旦把第10行的循环从1开始,就反过来了。所以会不稳定。

 11.2  Radix sort

基排序。
比如n个数字,每个数字有若干部分,先按照低优先级的部分排序,然后按照高优先级部分排序。排序时保证使用stable 排序(比如用counting sort),最终我们能够完成排序。
比如数字时年月日,我们可以先排日,然后月,然后年。
设有n个元素,每个元素都是b bits数据,设我们将每个数据占有的b bits按照每份r bits分成若干份,

其实,此问题可以简化成几轮counting sort。所以复杂度可以用o((b/r)(n+2的r次方)来表示, b/r表示需要排序的轮数。n+2的r次方表示元素的可能取值范围,类似counting sort中的k.

有r<=b, 那么复杂度:
当b<lgn时,O(n)
当b>=lgn时,复杂度的下限是o(bn/lgn). 当r=lgn时最优。当r大于或者小于lgn时,复杂度都高于o(bn/lgn)

和快速排序的比较:
当b = O(lgn)时,如果取r = lgn, 那么radix sort优于快速排序。
此外,快速排序的cache性能较优。并且时in place排序,不需要额外空间。
radix排序如果使用counting sort来实现,需要额外的空间。
 11.2.1  Exercise
blog.sina.com.cn/s/blog_ad77038501011gn6.html
www.cnblogs.com/luowei/archive/2008/01/21/1047020.html

一般来说,如果元素交换是相邻元素的交换,那么可以做到稳定。如果不是相邻元素的交换,那么很有可能就不稳定。

8.3.2 insert sort, merge sort, heap sort, quick sort是stable 的么?如何让他们stable.
Insert sort: stable的。插入元素和前面的已经有序的元素比较时,可将交换条件设置成是否大于前面元素。
Merge sort: 在merge时,可以保证稳定性,把相等的元素如果位置小,就优先安排。 Merge sort其实本质是把n个元素分成n个小部分,分别两两merge. 我们在merge时可以考虑元素原有的位置关系,以保持顺序。
Bubble sort: statble. 可以仅当元素大于后邻元素时,才将元素交换位置。可以保证稳定
Heap sort: 不stable,
quick sort: 不stable.
Counting sort: stable.
选择排序: 不稳定。找到一个小元素,会和之前某个位置元素换位,可能导致稳定性混乱:
举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法

shell sort: 虽然insert 排序稳定,但是shell sort采取了不同间隔的多个插入排序队列,导致破坏了稳定性。




 12  Chapter 15 dynamic programming
divide-and-conquer方法,是把问题分解为小问题,逐一解决,小问题互相内容不重复。所以用递归解决比较合适。
而dynamic-programming类似,也是分解为小问题,但是小问题,互相重复,overlap。也可像divide and conquer那样递归解决,但是会重复计算小问题,性能较差。需要特殊处理,于是就有了下面的阐述。

A dynamic-programming algorithm solves each subsubproblem just once and then saves its answer in a table, thereby avoiding the work of recomputing the answer every time it solves each subsubproblem.

多使用bottom-up的方式来解决,从小问题入手,记录小问题的结果,逐渐扩大问题规模,直到解决问题。
多用来解决最优化问题,比如求最大,最小值等。
特点: optimal substructure, overlapping subproblems
所谓optimal substructure意思是说,如果找到一个最优方案,那么这个方案中的子问题也是最优方案。因为他是从子问题的最优方案一步一步汇聚成大问题的最优方案的。如果不能保证这一点,就不适合dynamic programming.
另外,子问题之间要相互独立,不能相互影响。否则一个子问题如果选择了最优方案,导致另一子问题无法得到最优方案,就意味着此问题不能用dynamic programming.
dynamic 的一些特点:

dynamic programing的复杂度特点:

dynamic programing的running time取决于2个因素的乘积:总体上一共的subproblem的数量,以及针对每个subproblem需要做出的选择的数量。
 12.1  Chapter 15.1 Rod cutting problem
Our first example uses dynamic programming to solve a simple problem in decid-
ing where to cut steel rods. Serling Enterprises buys long steel rods and cuts them into shorter rods, which it then sells. Each cut is free. The management of Serling Enterprises wants to know the best way to cut up the rods.
We assume that we know, for i = 1; 2; : : :, the price pi in dollars that Serling
Enterprises charges for a rod of length i inches. Rod lengths are always an integral number of inches. The rod-cutting problem is the following. Given a rod of length n inches and a table of prices pi for i = 1; 2; : : : ; n, determine the maximum revenue rn obtain- able by cutting up the rod and selling the pieces. Note that if the price pn for a rod of length n is large enough, an optimal solution may require no cutting at all.
切钢材,不同长度的钢材价格不同,计算一个收入最大化的切法。
大问题化小问题,切长度n的钢材时,第一刀,有n-1种切法。那么这n-1中切法中最大收入的就是结果。
递归定义:
r(n) = max(p1+r(n-1), p2+r(n-2)....p[n-1]+r(1))
发现问题:r(i)的计算会重复计算r(i-1)....r(1).
那么需要记录下来小的问题的结果。
下面提供了3中方案,第一种低效的递归方案,大量重复计算。O(2n)
第2种也是递归,但是保存了小问题数据,性能提升。第三种非递归实现,性能更加提升,但是和2统一级别。都是O(n2)
一个有重复计算的低效的递归算法:








上面的q是用来记录最大值的。
复杂度:指数级别,因为:如下是划分n=4的钢材的recursion tree. 不断出现的重复子树,代表了重复的计算。

一个从上而下采用了递归但是也采用了dynamic -programming的高效的方法:



此算法虽然也是递归,但是对于重复的子问题,直接取出结果即可。结果保存在r里面。
复杂度是O(n2), 而不是第一种递归的指数级。
第一种的递归具有t(n) = t1+...+tn-1的关系。
但是这里的算法表面上看也是类似关系,但是实际上不同,因为,当小问题被计算过后,tn 的计算就是直接取出n-1个结果相互比较就可以了,在函数内直接在第2行就反回了,所以一次小问题的调用复杂度成了常量级别。那么问题复杂度成了函数调用次数的问题。所以一次迭代内部的函数调用次数是n-1次. 而每次调用都能立刻拿到了结果。 共迭代n次,所以也是O(n2)
下面是一个采用了dynamic programming的bottum up的非递归算法:比上面算法快一些,但都是同样级别。










此算法从小问题着手。此算法复杂度O(n2),因为有两个循环。每次循环的执行都是常量级别的计算。
 12.2  Chapter 15.2 matrix-chain multiplication problem
也是经典的dynamic programming问题。
为A1*A2.....*An找一个乘法运算最少的划分方案。
基础: A(i,j)矩阵和B(j,k)矩阵相乘得到C(i,k)矩阵,且共有i*j*k个乘法运算。
A*B*C 等价于(A*B)*C 等价于A*(B*C)所以需要找一个乘法运算最少的方案。

核心是先确定能分解成多个子问题,并取子问题的最优解。列出公式,进而列出递归算法。然后可以用bottom up的方法列出迭代算法。最后求出复杂度。
注意,划分子问题的方式:
A1*A2....An多个矩阵相乘的最优解相当于在(A1*A2...Ak)(Ak+1*....An) 且k在1至n-1之间,取最优解。

如果用brute force方法:列觉所有可能,那么:假设p(n)是n个矩阵相乘的乘法数目:需要如下乘法:

复杂度是o(2n)
如果用dynamic programing列出划分子问题的公式:
m[i,j]是Ai****Aj矩阵的最小乘法数目。K是i,j之间的一个值。
然后,可以很容易实现递归代码,我们也可以用迭代来实现:














主思想,m[1...n, 1....n]是数组,m[i,j]就是跟上面一个意思。L 代表长度,我们先从l为1, 2的小长度矩阵乘法开始,然后逐渐推倒出长度大的乘法数值。然后呢,i是矩阵区间的开始,j是结束,k是中间一个。当推导结束后,就知道m[i,j]了。
三层循环,L, I, K这三个循环变量的最大值都可能是n-1,那么复杂度是O(n3).
 12.3  Chapter 15.4 Longest comman subsequence (LCS) problem
求两个序列里面的最长的公共子序列。
动态规划算法,o(n2)

方案:比较最后一个元素,根据是否相等,分情况,将问题划分成向开始元素靠近的子问题,这样最终的子问题是接近开始元素的。便于用迭代完成。

公式:

迭代算法:
负责度O(mn)
LCS的类似的一个问题是LIS问题,可以使用LCS算法,但是不是最优算法,最优算法是O(nlgn):
 12.3.1  练习15.4.5和15.4.6 找一个数字序列中最长的增序子数列。longest increasing subsequence (LIS) 问题, 此问题是o(nlgn)的复杂度。
The Longest Increasing Subsequence problem is to find the longest increasing subsequence of a given sequence
解决方法1: 采用LCS算法,将序列排序得到另一各序列,然后求两个序列的LCS.
o(n2)
解决方法2: dynamic programming方法,o(n2)
方案:算出以每个元素aj结束的最长子序列的长度,最后遍历一遍所有a,找到最大的子序列长度即可。
于是,问题转化为求以aj为尾巴的最长子序列长度的问题。
对于aj左边的数字,如果有小于aj的数字,那么每个以这些数字为尾巴都有一个最长子序列的值, 我们找到其中最大的,+1后,就是以aj为结尾的最长子序列的长度。这样可以看到划分子问题,并且在子问题之间找到最大值+1就可以了,子问题之间没有依赖。符合optimal substructure.

递归算法实现:
q数组存储了每个元素为结尾的最大子序列长度。
function lis_length( a )
    n := a.length
    q := new Array(n)
    for k from 0 to n: //从头开始遍历所有元素
        max := 0;
        for j from 0 to k, if a[k] > a[j]: //遍历元素ak之前的所有元素aj
            if q[j] > max, then set max = q[j]. //找到最大的前面的元素结尾的子序列长度
        q[k] := max + 1; //算出以ak结尾的最大子序列长度
    max := 0
    for i from 0 to n: //比较所有元素结尾的最大子序列
        if q[i] > max, then set max = q[i].
    return max;
o(n2)

解决方法3:
此方法比较巧妙。
也是要找以某个节点结尾的最大子序列长度,方法不同于上面的解法2. 上面是遍历所有元素。而方法3有法子可以将这个查找过程降低为lgn级别。采用二分查找。在当前元素之前的各个长度的最大子序列的最小尾巴构成的有顺序的数列里面执行2分查找,找打一个位置,用来确定当前分析的元素为尾巴的最大子序列长度。
设Aij代表a1,a2...ai中找到的长度为j的子序列中,数值最小的尾巴元素。因为长度为j的子序列可能有多个,我们找到尾巴数值最小的元素,就比较有利于将来找最长的子序列。
可以证明ai1<ai2<ai3...<aij. 意思是长度为2的子序列中最小的尾巴 < 长度为3的子序列的最小尾巴。因为,如果如果ai2  > ai3,那么ai3所在的长度为3的子序列中的前趋元素就可以成为ai2了,且满足小于ai3, 这就矛盾了。

还可以证明, ai1, ai2,,,aij构成了一个最大子序列。因为他们满足上述大小关系。
如果我们遍历已知的ai1,ai2....aij 并找到一个位置 使 aij < ai+1 <= ai,(j+1). 那么意味着,ai+1比aij大,所以可以和ai+1合作构成一个长度为j+1的子序列。又因为ai+1<= ai,(j+1)意味着,这个ai+1比目前算出来的长度为j+1的子序列的最小尾巴还要小,那么就可将a+1更新成最新的a(i+1),(j+1)就是在a1....ai+1的范围内,长度为j+1的子序列的最小尾巴。
可以证明:这个算出来的j+1就是以ai+1结尾的最长子序列。
如果有以ai+1结尾的更长的子序列,比如在在a1,....ai中就存在一个元素ax小于ai+1, 且经过该元素有长度j+1的子序列。 根据上面的条件可知,ai+1 <= ai,(j+1) 意思说我们找的这个位置,或者说找的这个j 使得ai+1 小于等于a1....ai中长度为j+1的子序列的最小尾巴。那么ai+1肯定小于ax. 这就和ax 是ai+1的一个前趋矛盾了。所以,不存在以ai+1结尾,长度超过j+1的子序列。

这样我们就知道了如何查找以某个元素结尾的最长子序列的长度是如何计算得了,在前面的ai1,....aij中找个位置,确定j, 那么j+1就是ai+1为尾巴的最长子序列长度。查找时采用2分查找,那么复杂度就是o(nlgn).


答案在这里:
http://www.algorithmist.com/index.php/Longest_Increasing_Subsequence

https://en.wikipedia.org/wiki/Longest_increasing_subsequence
http://www.cnblogs.com/lonelycatcher/archive/2011/07/28/2119123.html
http://www.ahathinking.com/archives/117.html
 12.4  problems
 12.4.1  15-1 DAG中的最长路径问题
https://en.wikipedia.org/wiki/Longest_path_problem
在任意没有限制的图中找最长路径问题是一个np-hard问题。而在DAG中找最短路径或者找2点间最长路径问题,是可以解决的。

(1)如果在有权重的DAG中找两点间的最长路径,方法如下:
在边有weight的有向无环图DAG中找两个点s,t之间的最长路径。
可以将每个边的权重取反,找最短路径,算法同dijkstra算法,greedy 算法。
https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

(2)如果在DAG中找所有路径中的最长路径(没有限制点),方法如下:
这个问题其实是critical path问题,也就是计算一个项目的若干个milestone之间,需要解决此项目所至少需要耗费的时间问题。

方法是:
1. 拓扑排序
2. 从头遍历每个节点,遍历所有进入此节点的前趋节点,找到(前趋节点的value+边的value)最大的组合,将该值作为本节点的value.
3. 遍历完所有节点后,找到value最大的节点,也就是最长路径的终点。回朔找到起始点。需要用某种数据结构记录前趋节点。
负杂度是线性的。
 12.4.2  拓扑排序
拓扑排序的方法:
https://en.wikipedia.org/wiki/Topological_sorting
http://blog.csdn.net/dm_vincent/article/details/7714519

排序方法1:Kahn算法
    1. 找到没输入边的顶点,将他们加入list。
2. 当list不空,从list中取出一个点, 将此点记录在结果R中。去掉他所有的出边,并判断出边链接的后续节点是否成为没有输入的顶点,如果是,也加入list中。
3. Goto 2.直到list为空。
4. 如果还剩下边,说明有环。否则结果已经得到R中。

此算法判断定点是否有入边,不太容易判断,对存储结构有要求。
此算法复杂度O(|V|+|E|), 因为首先过一遍所有节点,找入度为0的节点,O(V), 然后在L中取一个节点标记,设此操作耗时t. 将该节点的所有边去掉设每去一个边,也耗时t. 那么如果有新的入度为0的字节点,也入list, 那么最终所有节点都会被处理一遍,耗时Vt, 所有边都被去掉一遍,耗时Et,所以,复杂度是O(|V|+|E|).

排序算法2:DFS算法
采用深度优先遍历,得到一个逆向的结果。
算法如下:
L ← Empty list that will contain the sorted nodes
while there are unmarked nodes do
    select an unmarked node n 任选一个节点
    visit(n) 深度遍历n, 完成后,L中节点按照依赖顺序排好了队。对于另外一些没有被处理的节点,肯定是有另外的入度为0的节点,那么后面的循环会将那些节点放到队列头部。
function visit(node n)
    if n has a temporary mark then stop (not a DAG)
    if n is not marked (i.e. has not been visited yet) then
        mark n temporarily
        for each node m with an edge from n to m do
            visit(m)
        mark n permanently
        unmark n temporarily
        add n to head of L //这里很重要,即保证了正确的顺序,也保证一些入度为0的节点被放到前面。


拓扑排序的应用,来找shortest path:
拓扑排序还可用来实现高效的最短路径查找。查找从某个节点开始的最短路径。
先拓扑排序,然后按照排序后的节点顺序,更新所链接的下一个节点的路径距离。具体方法类似dijkstra算法。
假设s是起点。但是s并不一定是拓扑排序后的起点,可能是中间某个点。排序后,将s点的距离设为0,其他点都设为无穷大。
Let d be an array of the same length as V; this will hold the shortest-path distances from s. Set d[s] = 0, all other d[u] = ∞.
Let p be an array of the same length as V, with all elements initialized to nil. Each p[u] will hold the predecessor of u in the shortest path from s to u.  p用来记录路径。
Loop over the vertices u as ordered in T:  按照拓扑排序结果从头开始遍历节点
For each vertex v directly following u (i.e., there exists an edge from u to v):
Let w be the weight of the edge from u to v.
Relax the edge: if d[v] > d[u] + w, set
d[v] ← d[u] + w,
p[v] ← u.

此算法 比简单的dijkstra算法高效之处在于,选择节点时,秩序按照拓扑顺序选择,而不用遍历找距离最小的当前点。估计拓扑排序可以保证一点特性: 拓扑排序后的节点,从起始点向后遍历,能够算出准确的每个点的距离。
 12.4.3  Problem 15.2 Longest plindrome subsequence (LPS)
最长的对称子序列(子序列不许要连续)

也是用DP。
模式:  设L(0,n-1)是最长序列,那么当x[0] == x[n-1]时,L(0,n-1) = L(1,n-2)+2, 否则,
L(0,n-1) = max( L(0,n-2), L(1,n-1)).

答案:
http://www.geeksforgeeks.org/dynamic-programming-set-12-longest-palindromic-subsequence/

此问题另一解法: 将字串反转,然后求LCS, longest common subsequence.
 12.4.4  Problem 15.3 Bitonic euclidean traveling-salesman problem

http://blog.csdn.net/yxc135/article/details/9570501

下面中文资料的公式有写错误,但是对问题的中文描述解决方法可以看看作为参考。
http://fangxia722.blog.163.com/blog/static/317290122008889326504/

bitonic tour问题:
https://en.wikipedia.org/wiki/Bitonic_tour
就是TSP问题是要找最短的经过所有节点的路径,是NP-C问题,经过对问题简化,比如先向左,再向右,然后回到起点的方法,可以找到一个较为不错的解。这种方案就是bitonic tour问题。
此问题解决方案是:
设共有n个点,按照x坐标排序后为x1,...xn。x1是最左点,xn是最右点。
假设两个人A,B同时从x1开始走,并且不走重复节点。假设任意时刻,A,B分别位于i, j位置。为简化问题,设i <= j-1.  那么B会先到达最右点。
设f(i,j)的意思是两者目前分别走到i, j位置后的走过的最小路和。
(1)如果, i < j-1,  那么意味着B拉开了A 2个或以上位置,那么可以肯定,j-1这个点肯定在B的走过的路径里面,所以 ,f(i,j) = f(i, j-1) + d(j-1,j) 其中d()代表两个点之间的距离。
就是说问题被缩小化为f(i,j-1)问题,加上B走过的(j-1, j)路径长度。
(2)如果,i == j-1, 那么以为A占据了j-1位置,那么B的前一位置只可能是 1....j-2之间的任何一个都有可能,那么我们可以取其中的最小者。
所以有f(i,j) = f(j-1,j) = min (f(i,k)+d(k,j)) == min(f(k, i) +d(k,j)) == min(f(k,j-1)+d(k,j)), 1<=k < j-1
上面因为f(a,b) == f(b,a), 我们只关心长度,不关心方向。
我们要找f(n,n) = f(n-1, n) +d(n-1,n)

再分析一下第一个链接,看路径是如何打印出来的。
具体伪码:

如何把路径打印出来,因为r[x,y], 当x<y-1时,存储的就是y的前趋,所以我们可以从r[1,n]开始,打印出一条路经的所有节点,剩下的节点按照横坐标顺序打印出来即可。上述网站上的打印逻辑我没有看懂。
 12.4.5  15-4 printing neatly problem
就是打印一串词语,词语间一个空格,每个词语长度已知,每行的长度已知,求一种打印方案,使得空格数目最少。最后一行后面余留的空格不考虑。

答案:
http://blog.csdn.net/yxc135/article/details/9621643
基本方案就是列出一个cost(j) 公式,j代表第j个词语,使cost(j)最小。
M是一行的长度。
cost(j)等于排列1...j词语的cost ,即cost(i-1) + 将i..j列一行的cost. 如果j = n, 那么将i..j排列一行的cost是0.
用extra(i,j)表示将i...j排列到一行的cost. 也就是剩余的空格个数 M – 所有词语长度 – (j-i) 个空格。此值可能是负数,表示乘不下i...j。
cost(j) = min( cost(i-1) + extra(i,j) ), 1<= I <=j-1。
特殊情况: 当extra(i,j) 是负数时,此方案没有意义,那么我们就让他为无穷大,从而从我们的方案中排除掉。当j=n时,如果extra()>0, 那么意味着i,j可以排成一行,且j是最后一个词语,那么就意味着这是最后一行,那么extra(i,j) 就 =0.
可见cost数据从小的数开始算起,知道cost(n).
在min比较时,我们找到cost最小的i, 也就是我们的一个选择。可以记为p[j] = min_i.
意思是排列1...j个词语时,最后的一行从min_i开始到j. 这样我们可以从p[n]逆向找到我们的所有方案。
具体的解法见上述网页。
 12.4.6  15-5 Edit distance problem
就是把一个字符串转换成另一字符串,可以通过insert, delete, replace等操作完成,这些操作都有cost, 求最小cost转换方法。


http://blog.csdn.net/abcjennifer/article/details/7735272
基本思想,设cost[i,j] 是从x[1...i] 转换成y[1...j]的最小cost. 那么最后一步转换可以有不同的选择,且cost[i,j]是这这几种选择之中的最小者。从而将问题转换为子问题,比如cost[i-1,j-1], cost[i-1,j], cost[i,j-1]等问题。最终可以用递归的方式,bottom up来计算出cost[i,j], 并可以记录下来每次操作,逆向打印出操做序列。

Edit distance和DNA的应用。
http://www.tuicool.com/articles/7Z3MZr
http://www.cnblogs.com/grenet/archive/2010/06/01/1748448.html
http://www.geeksforgeeks.org/dynamic-programming-set-5-edit-distance/
 12.4.7  15-7 viterbi algorithm
一种引入了概率的算法,如果物体可以在一些列状态间转换,知道物体初始状态的概率,状态间转换的概率,以及在各个状态下物体表现出特定现象的概率。如果知道一个现象序列,那么推算物体最可能的内部状态转换序列。
https://en.wikipedia.org/wiki/Viterbi_algorithm
http://www.52nlp.cn/hmm-learn-best-practices-six-viterbi-algorithm-1
http://blog.sina.com.cn/s/blog_5a5e480b0100fyi5.html
也是dp算法。
此算法多用于speech recognization.
假设状态集合S如下:s1,....sn.
每个状态的初始概率是pi,  1<= I <=n.
从状态i转换到状态j的概率是pij.
物体有m种现象, o1....om.
物体在状态i下表现出j现象的概率是p[i|j].
已知,对物体作出T次观测得到的现象序列o1,....ot,
求物体最可能的内部状态变换序列。

答案:
设Vt,k 表示物体在第t次观测时,状态是K的最大概率。为啥说最大概率,因为物体如果第k次观测是状态k, 也是有很多种状态转移序列达到此状态的,而每种序列的概率不同,我们想求出最大的概率序列。
如果我们求出所有状态的该值,也就是vt,1, vt,2..... vt,k, 那么也就是说我们综合考虑了上述输入和物体的状态变换,算出了物体最终到达各个状态的最大概率,找到其中最大的那个概率,他对应的状态就是物体最终最可能所在的状态。然后再找最可能进入该状态的前趋状态,从而逆向找到最可能的状态转移序列。

关键是vt,k的构建。
因为物体在第k次观测成为状态k有好多可能,因该是k种可能,也就是有k种前趋状态。那么转化为子问题:
vt,k = max ((vt-1,i)*pik*p[k|ot]), 1<= I <= k.
vt-1,i 是到达前趋状态的最大概率, 也就是子问题。
Pik是从前趋状态到k状态的概率。
p[k|ot]是在k状态下物体表现出ot现象的概率。
我们是求上述若干的前趋的计算数值最大的,这个找到的前趋就是k状态的前趋。

由此可以知道子问题的划分,就可以求出所有的vt,k组合了。
所有vt,k组合中最大的那个对应的状态k就是物体最可能的最终状态。
其前趋状态也在上面可以求出来。
Wiki上的例子:
Consider a village where all villagers are either healthy or have a fever and only the village doctor can determine whether each has a fever. The doctor diagnoses fever by asking patients how they feel. The villagers may only answer that they feel normal, dizzy, or cold.
The doctor believes that the health condition of his patients operate as a discrete Markov chain. There are two states, "Healthy" and "Fever", but the doctor cannot observe them directly, they are hidden from him. On each day, there is a certain chance that the patient will tell the doctor she is "normal", "cold", or "dizzy", depending on her health condition.
The observations (normal, cold, dizzy) along with a hidden state (healthy, fever) form a hidden Markov model (HMM), and can be represented as follows in the Python programming language:
states = ('Healthy', 'Fever')
 
observations = ('normal', 'cold', 'dizzy')
 
start_probability = {'Healthy': 0.6, 'Fever': 0.4}
 
transition_probability = {
   'Healthy' : {'Healthy': 0.7, 'Fever': 0.3},
   'Fever' : {'Healthy': 0.4, 'Fever': 0.6}
   }
 
emission_probability = {
   'Healthy' : {'normal': 0.5, 'cold': 0.4, 'dizzy': 0.1},
   'Fever' : {'normal': 0.1, 'cold': 0.3, 'dizzy': 0.6}
   }
 12.4.8  15-8 image compression by seam carving
一张图片,m*n个像素,想给每行去掉一个像素,每个像素都有一个权重w[i,j],代表了去掉它对图像的影响。相邻行的被去除的像素需要是相邻或者最多差一个位置的像素。如果用穷举法,是指数的复杂度,列出一个高效的算法来。
答案:如果用穷举法,第一行有n种可能,针对每一选择,下一行,有3种选择,从而一共有n*3*3....所以是3m的指数级别复杂度。

用dp算法的好处在于避免重复计算,那么重复在哪里?比如我们在穷举法中,有两个方案A, B他们都使用了第i行,第j列的一个像素,那么可能他们下面的行也使用了相同的选择,仅仅是i行之前的行使用了不同的像素。但是我们还是重复计算了i行以及以下行的成本和。好,那么我们的算法就是要避免重复计算。

我们从顶往下选择,
定义a[i,j]代表: 如果我们选择了i行,j列的像素作为我们的方案,那么从第一行开始直到第i行的所有选择所购成的最小权重值。就是说,因为我们从第一行开始选择,当我们选到了第i行了,说明0,,,i-1行全选完了,那么哪些选择的权重之和就是代表了a[i,j].
因为i-1行有三种可能,分别是a[i-1,j-1], a[i-1,j], a[i-1,j+1], 所以:
a[i,j] = w[i,j] + min(a[i-1,j-1], a[i-1,j], a[i-1,j+1]).
由此,我们可以从a[0,0]开始构造,注意,靠边的像素的前趋选择只有2种可能。
算法复杂度o(n2)
 12.4.9  15-9 breaking a string
一个字符串,n个字符,从中任意位置截断的cost是n. 也就是说和字符串长度一样。如果指定了m个截断位置,问最小的cost是什么?


答案:
注意因为我们的字符串实际下刀的位置是明确的,所以有些任意的i,j区间是不存在的。
我们应该这样做:
因为我们有L[1..m]个切点,所以我们有m+2个关键位置,包括开头,结尾,以及中间的m个切点。那么我们应该让i, j代表这m+2个关键点。这样依赖,i, j, 相互+1, -1都是在关键点之间变化,而k的取值也是在关键点里。
设cost[i,j] = 截断从i开始到j的区间的一共的成本。
len[i,j] 表示i,j区间的长度
cost[i,j] = len[i,j] + min( cost[i,k]+cost[k,j]) , i< k < j, 且k属于争取的截取位置。
可见,大区间i...j的问题,被转换成小的区间i..k, k...j的问题。
我们从最小的原始区间开始。cost[i,i] = 0, cost[i,i+1] = 0,因为这个小区间并不会被切, cost[i, i+2] 的值就是i+2和i关键点之间的字符个数,而且切点只有一个就是i+1.
。。。。依次类推。可以扩大推算出const[0,n-1].
先算出跨越2个关键点的所有区间的成本,然后是跨越3个关键点的,。。。。直到算出跨越所有关键点的成本。bottom up.
 12.4.10  15-10 planing an investment strategy
有一笔钱可以投资,已知n个投资项目,可以投j年,知道每个项目在j年的利润率为dij, 表示第i个项目在第j年的利润率,每年年初投资,年底拿到钱,来年可以换项目,也可一不换,如果换项目,需要缴纳f2的手续费,如果不换,缴纳f1, f1<f2. 求j年后的最大利润以及投资方案。

首先,这的确是dp问题,optimal substructure如下:
如果第j年拿到了最高的利润,那么j-1年也是一样,否则可以替换j-1年的方案。
P[i]代表第i年拿到的利润。
S[i] 代表第i年所选择的项目
R[i,j]代表第i项目第j年的利润率
Rmax[j]代表第j年率润率最高的项目编号
P[i] = 如下几项中最大的:
1. 如果第i年保持和第i-1年一样的项目,那么:p[i] = (p[i-1]-f1)*R[s[i-1],i]
2. 如果换了项目,那么要选利润最高的: p[i] = (p[i-1]-f2)*R[Rmax[j],j]

那么可见p[i] 被转换成了p[i-1]。那么从第一个开始,就可递推到p[j]
 12.4.11  15-11 inventory planning
一个工厂,知道将来n个月的需求分别为di个产品,固定工人,每个月可以生产最多m个产品。如果另外雇佣临时工,每生产一个产品,成本为c. 如果月末产品有剩余,则存放i个剩余产品的成本是h(i). 求怎样安排生产,才时工厂这n个月的成本最低。

答案:列出模型:
left[i] 为第i月的产品剩余。
cost[i]为第i月的成本。
cost[i] = 三种情况:
1. 上个月遗留的产品大于本月的需求,就不许要再生产:left[i-1]>=di, cost[i]就是需要保存剩余物品的成本,即h(left[i-1]-di), 且left[i] = left[i-1]-di.
2. 上个月遗留产品加上固定人员的生产能力足够本月需求,即left[i-1]+m >= di, 那么本月不许要雇佣临时工,也不会剩下产品。cost[i] = 0, left[i] = 0.
3. 上个月遗留的产品加上固定人员生产能力不足本月需求,需要再雇佣临时工,即left[i-1]+m<di, 那么cost[i] = c*(left[i-1]+m-di) 也就是临时工成本, 且left[i] = 0.

可见cost数组其实是依赖于left[]数组的计算的,而left[i]数组依赖于left[i-1]的子问题,所以可以bottom up计算。
 12.4.12  15-12 Signing free-agent baseball players
一个俱乐部,给定一笔钱,给定n个位置,每个位置上都有p个球员可供选择,已知每个球员的价码,以及球员的价值,要求允许某些位置不买球员,不能超过预算,尽量价值最大化。这个类似于0-1背包问题。

答案:
设cost[i,j]代表第i位置,第j个球员的价码。
value[i,j]代表第i位置,第j个球员的价值。
共有1...n个位置。
f(m,n)代表用钱m购买1...n位置球员的最大价值和。
我们倒着递推公式:
有如下集中情况:
如果第n个位置我们不买球员,那么f(m,n) = f(m, n-1).
如果第n个位置我们打算买球员,那么f(m,n) = max( value[n,i] + f(m-cost[n,i], n-1)), 且1<= I <= p, 且m > cost[n,i] 就是说我们要在位置n上的1...p个球员中选一个,选的这个球员要求加码小于我们现在有的钱m,根据此p种可能,找一种价值最大化的方案。
两种情况综合在一起,就有:
f(m,n) = max( f(m,n-1),  value[n,1] + f(m-cost[n,1], n-1),  value[n,2] + f(m-cost[n,2], n-1).... value[n,p] + f(m-cost[n,p], n-1)).
可见m,n开始的问题,转化为m, m-..., n-1,...的子问题。
因为虽然每个球员的价值并不是相差1,但是为了计算的方便。我们从底向上计算时,每次m增加100000美元,以单位1来计算。可能会有多余的不合理的计算,但是至少能够算出来。
 13  chapter 16 greedy algorithms
有些算法可以划分成子问题,从而用递归或者非递归的dynamic programming完成。但是可能并不是最优的方案。如果问题还具有局部最优化的特点,也就是说具有greedy algorithm的特点,那么采用greedy 算法,可能更快。
Greedy 算法能够解决的问题的特点: 问题能够找到一个greedy choice, 使问题小一点,且能证明该greedy choice的确是最优的解。那么此问题就可一用greedy 算法。
一般能用greedy 算法的问题都可以使用dynamic programming来解决,相反,有的问题只能用dynamic programming来解决,而不能用greedy解决。譬如,0-1背包问题只能用dynamic programing, 而可以取部分物体的背包问题可以用greedy来解决。
 13.1  Activity selection problem
例子:
下面假设我们的所有的事件已经按照结束时间排序。注意,有些overlap的事件是不能被安排进来的。

使用dynamic programming模型的低效算法:
c[i,j]表示a1之后,aj之前能够安排的事件的最大值。
那么当i=1, j=n, 那么此公式就是我们要得解。
由于中间会有subproblem overlap, 我们把他们的记过记录下来。
算法执行下去后,如果从树的递归树角度分析,那么最终走到叶子节点后,开始往上走,每次根据已经计算了的所有子问题可以推导出一个大级别的区间的最大值时,需要针对该大区间内部所有事件做一次遍历。而一共我们需要作出多少种区间呢? 也就是所有i,j的组合,因该是n*(n-1)种。那么每种内部都有一个针对区间内所有事件的循环来找最大值,这个内部循环因为是利用已经保存的数据,所以复杂度是n. 所以猜测总体的算法复杂度o(n3).

如果我们不记录中间子结果,那么怀疑复杂度是指数级。因为此算法模式和dynamic programming中的模式类似。

使用greedy 的高效递归算法:
每次找最早结束的事件。
s, f是事件的起始,结束时间数组。k是上次已经选中的事件的索引,所以我们接下来需要选择开始时间在k事件之后的事件。N是全部事件的数量。
1,2,3就是先找一个结束事件最早并且在事件k之后的事件。
5行就是把此事件于子问题的结果合并出最终结果。
复杂度o(n)

使用greedy的高效迭代算法:
思想同上,









复杂度o(n)

另一种方法:不是每次都找最早结束的事件,可以每次从后面找最晚开始的事件。也是最优解。为啥?我们可以把时间倒流的方式来看待这个问题。所以本质上是一样的。最晚开始,也就意味着如果时间倒着流的话的最早结束。

 13.1.1  练习16.1.4


如果有多个会议室,且要求最终使用最少的会议室。
我的思考:
如果正向思考,先用上面方法把一个会议室填满,再填下一个,可能不是最佳。的确不是最佳。
反向思考,有些事件互相有overlap,肯定不能在一起,那么第一步,把overlap的事件,分到不同的会议室。
剩下一些没有overlap的事件,并且和会议室中已经分配的事件都没有交集,其实分起来就相对容易。按顺序从第一个会议室分配即可。
那么关键就是如何分配互相overlap的事件。我们要先建立互斥关系。然后安排他们到不同的会议室。当然尽量减少会议室的使用。
首先计算所有的具有互斥关系的事件对儿,耗时o(n2).
然后遍历所有互斥关系的事件对儿,安排到不同的会议室。
然后在顺序遍历剩下事件,安排他们,后两步耗时o(n). 总体耗时o(n2).

人家牛逼的算法答案,o(n)级别:
取出所有事件的开始和结束时间,构造一个数列,然后按照时间顺序排序。 o(nlgn)
维护两个会议室链表,一个表示free list, 一个busy list表示针对当前时间t是free还是busy的会议室的链表。
循环遍历排序后的时间,针对t, 如果t是一个事件的开始,那么在当前free list中安排此事件,并把该会议室挪到busy list. 如果t是一个事件的结束时间,那么将该会议事从busy list中挪到free list头部。
这样刚开始所有会议室都在free list. 这样就以遍历一遍所有事件就可完成安排。o(n)复杂度。
我草,真牛比。比较所有的事件都要被安排,那么就按顺序来吧。
 13.2  0-1 knapsack problem和fractional knapsack problem. 0-1背包问题和能够取部分物体的背包问题
0-1背包只能用dynamic programming
fractional 背包可以用greedy.


excersice 16.2-2 提供一个0-1背包问题的dynamic programing 解决方案,复杂度O(nw):

0-1背包dynamic programing方案:




问题首先用数学方式推导公式,然后发现具有数组的小元素=》大元素的推导关系,每次推导耗时0(1),需要推导的次数就是数组的大小,也就是o(nw)
 14  chapter 24 single source shortest path
解决从一个特定原点到其他定点的最近路径问题。可以反过来看成找到所有从其他定点到达某特定定点的最进路径问题。
Dijkstra算法要求图中没有nagative edge. 而bellman ford算法,允许nagative edge的存在,并且如果有nagative cycle,也能够发现并报告出来。并且只要原点无法达到nagative cycle, 那么也能算出从原点到其他合理节点的最短路径。
 14.1  Chapter 24. 1 Bellman-ford algorithm
允许有向图中存在负环。可以发现。
较为简单,因为G = {V, E}里的最短路径,理应最多有|V|-1条边。因为如果该路径把所有节点都经过一遍,也就|V|-1条边。所以如果我们执行|V|-1次循环,每次循环遍历处理所有的{u,v}边,处理逻辑就是根据最新的u点的路径距离,更新v点的路径距离。那么这么多循环过后,理应找到所有节点的最短的距离s的距离。
初始值: s的距离0, 其他节点的距离无穷大。
那么如果执行了|V|-1次循环之后,我们再执行一次循环,如果发现还有节点v的距离在变小,那么意味着从v,到v的前趋肯定在一个negative cycle里面。我们往前找前趋,应该能够找到一个cycle.
如果从s点可以遇到negative cycle, 那么就无法求出从s点开始,到所有点的最短路径,那么算法可以返回失败。








输入G是图,w是权重函数,s是原点。initialize_single_source是初始化。
relax(u,v,w)是跟新u,v边里v点的距离。
v.d 就是v点的距离.






v.л代表v点的前趋。




复杂度O(VE)
 14.2  chapter 24.2 求DAG有向无环图的最短路径(利用拓扑排序降低复杂度)
由于没有环,那么该图就可拓扑排序,在基础上修改bellman-ford算法,不再执行|V|-1遍所有边的遍历,而是遍历一遍拓扑排序后的节点,针对每个节点,处理一下从该节点的出边,就可完成任务。时间复杂度 O(V+E)







 14.2.1  应用DAG的最短路径算法求最长路径,解决PERT chart的critical path问题
https://en.wikipedia.org/wiki/Critical_path_method
DAG拓扑排序后,如果求图中的最长简单路径,就是相当于找critical path. 可以把DAG看作一个项目的若干个子job. 那么critical path代表了完成项目所需要的最短的时间。可以合理安排各个子job的工期。
求DAG的最长路径方法是把各边的权值改为负数即可。

如何得到一个dag graph中所有path的数目。
 14.2.2  如何计算DAG中,从一个点开始到另一各点的所有路径
模型: u->v的所有路径是u的所有孩子ci分别到v的路径之和.
dp算法,并且将子问题保存起来。
 14.2.3  练习24.2-4 找一个DAG中所有的path
Give an efficient algorithm to count the total number of paths in a directed acyclic graph. Analyze your algorithm .
所谓所有的path, 我觉可以通过计算出到达每一定点的path的数目,然后求和。
在求最短路径的算法中,我们是找到达一个点的路径中最短的,那么其实我们已经遍历了到达一个点的所有路径,只需把它们记录下来。
模型: 因为有很多点,i->j的不同路径就有很多种。
我们想到从path终点入手,求出到j的所有路径,然后将所有j的path求和。
那么问题转换为求到一个特定点的所有路径之和。简化一步。
pathcnt[j] = sum( pathcnt[i] ) + 前趋的数量 , i是所有具有i->j的边的定点。之所以加上前趋的数量是因为允许有从前趋开始的路径
pathcnt[k] = 1, 如果k没有入边。
对于DAG来说肯定有定点没有入边,否则就有环了。
那么算法如果从bottom up来计算的话,相当于从所有入度为0的定点开始延伸出各个定点的path数目。知道所有定点的计算完成。最后sum一起。
我们不妨先将图拓扑排序一下,这样开始的定点就应该是入度为0的定点,顺着该定点向后延伸,便于算出到某点的路径数量。
top_sort(); //耗时O(|V|+|E|)
for( v in sorted_v ) |V|次循环
     get v;
      if( v.入度 == 0 )
             pathcnt[v] = 1;
       else  {
        pathcnt[v] = sum( pathcnt[pi] )+v的前趋的数目, pi是所有v的前趋。因为我们已经拓扑排序,所以,这些数值都是已知的。所有循环完毕后,这里相当于执行了|E|次加法。
    }
   
最后for all vertex v,
       sum (pathcnt[v])从而得到所有的路径的数量, |V|次加法
那么一共执行了O(|V|+|E|)时间。
 14.3  Chapter 24.3 Dijkstra's algorithm
基本思想:每次从剩余的定点找距离最小的,添加到另一集合,并根据该节点的距离更新他的相邻节点的距离。直到将所有节点都处理完。是greedy 算法。用来从剩余节点查找最小距离定点的算法extract_min()的实现会影响到整体算法复杂度。
可以采用简单查找,priority_queue(比如用binary heap, 或者fibonacci heap)来提高查找效率。









初始化操作: 将原点的距离设置为0, 其他都设置为正无穷大。
 14.3.1  练习24.3-2
为什么dijkstra算法不允许有负边。
Give a simple example of a directed graph with negative-weight edges for which
Dijkstra’s algorithm produces incorrect answers. Why doesn’t the proof of Theo-
rem 24.6 go through when negative-weight edges are allowed?
证明为什么dijkstra算法不能允许图中有negative weight edge?
因为dijkstra算法成立的前提是他的greedy 策略依赖一个规则:
在任意时刻,从剩余的定点中挑选距离最小的顶点,那么该定点的距离就是到它的最小距离。
这个可以证明,因为我们知道剩余定点x的当前距离,是目前已经知道的定点中到达x的最小距离,如果有到x的更小的距离,那么只能是通过剩余定点中另一各点,比如y. 之后在从y到达x. 正因为我们规定所有边权重为正,所以我们可以肯定,这样的y是不存在的,如果存在,那么y的当前距离肯定比x的当前距离小,那么就因该选y而不是x了。
而如果我们允许有负边,那么就不能保证当前距离最小的定点x的最小距离是当前距离了,因为的确可能有一条到另一各剩余定点y的路径,然后再一条从y到x的负权重路径,导致虽然y的当前距离大于x的当前距离,但是y的距离加上负路径最终会小于x的当前距离,那么我们的dijkstra算法就无法使用了。
 14.3.2  练习24.3.-6
一个图,点之间的权重是路径的可靠性,<1, 球最可靠的路径。
关键是弄清出什么是最可靠的路径,以及路径的可靠性计算方式。
在一个路径中,一个点v的可靠性 = 该路径中v点的前趋u的可靠性*(u,v)的可靠性。
我们用dijkstra算法稍作修改,就可一完成本任务。
初始化: 原点的可靠性是1, 其他都是0.
每次找可靠性最大的点,并根据出边,更新相邻定点的最大可靠性。
然后再找可靠性最大的点。....
 14.4  chapter 24.4 difference constraints and shortest paths
这一章比较难理解,大致意思是,设A是一个m*n矩阵,b是一个m维向量,A, b是已知的,求一个向量使:
Ax <= b成立。这个问题等价于求一个向量x = {x1, x2....xn}
这个问题等价于构造一个图,图中n个定点,m条边。每个定点对应一个xi, 每条xi->xj的边对应Ax <= b所描述的xi, xj之间的关系。增加一个v0定点能够到所有其他定点。
那么求上述解就等价于求从v0到所有xi定点的最短距离。
可用bellman-ford算法求出来。
也就是说靠图的最短路径算法来解决difference constraints矩阵的向量解。
这个矩阵可能并不是一般矩阵可能是使得:
Ax <=b 等价于 x1 – x2 <= b1, x2-x3<=b2 ….等等类似的模式的简单的矩阵。那么就靠图中的边来描述xi 定点之间的这种大小插值关系。边的权重就是bi.
 14.5  练习
 14.5.1  Problem 24-3
套利问题,就是多种货币,汇率不同,尝试找到一种兑换方案:从一种货币出发,不断兑换,最终再次兑换成本货币,使得能够套利。
其实是将问题转换成bellman-ford算法,依靠bellman ford算法能够确定图中是否有negative cycle的特点,让这个negative cycle成为套利的方案。
答案:


 14.5.2  bitonic shortest path
一个图,求原点s到其他点的最短路径,已知一个特点:所有的最短路径都满足bitonic sequence. 就是说先单调增加再单调减少。求较高效率的算法。
答案:因为我们知道在bellman-ford算法中,通过|v|-1次循环,每次循环针对所有的边执行一次relax算法就可保证所有最短路径都被计算出来了。
我们还知道,如果有一个u->v的最短路径经过了e1, e2....ek条边,如果按照顺序把e1....ek边relax一遍,那么就能够找到该最短路径了。(这个特点来自chapter 24.5中lemma 24.15 path – relaxation properity)。现在该最短路径是双调路径,那么又因为所有的边都是唯一的,好,我们把所有的edge排序后,正向来一遍relaxation,反向来一遍relaxation就可以了。这就可以保证我们针对所有的最短路径,都按照他们的顺序调用了relaxation操作了。
 15  chapter 25 all pairs shortest path
之前有dijkstra算法和bellman-ford算法来计算从一个原点,到其他点的最短路径问题。
这里指的是计算一个没有nagative cycle的图中,任意两个定点之间的最短路径问题。
表面想一想,针对每个定点,调用dijkstra算法,可以完成任务,但不是最优解,因为肯定会有重复计算。
所以这里的方法用矩阵来描述所有点点之间的最短距离。将算法描述成矩阵之间的计算和演化。
 15.1  chapter 25.1
最直接的dp算法
此算法其实本质上和bellman-ford一样,只不过记录了中间结果。效率高一些。
模型:  i->j的最短边, 和i->k的最短边+w(k,j)之间的关系。而bellman-ford算法也恰恰是使用这个关系。靠m, m-1, m取n-1来代表的路径占用的边的数目来控制循环次数。
i->j的最短距离最多有n-1条边。
那么i->j的占用了m条边的最短距离是如下的最小者:
1. i->j只占用m-1条边的最短距离
2. i->k占用m-1条边的最短距离+w(k,j)
可见当m= n-1时,我们就算出了所有最短距离。
可见大问题被转换成了小问题。
复杂度o(n3)
初始条件,i->i的最短距离是0.初始i->j的最短距离是正无穷。





下面函数执行真读所有i,j组合,执行一次上述计算。但是我们需要执行更多次。










上面函数多次调用extend-shortest-path来算出最终的结果。
如何找到具体的路径? 在extend_shortest_path函数中,当l'ij 被更新时,就记录下此i,j对应的k. 他就是目前发现的从i->j最短路径中的j的前趋。

如何判定存在negative cycle?
当我们的L(n-1)算完后,再算一次,如果有元素的l'ij还在变小,就意味着存在negative cycle. 这种逻辑和bellman-ford算法中判断单原点的最短路径中是否存在negative cycle类似。
 15.2  Chapter 25.2 floyd-warshall 算法
另一个dp算法,复杂度也是o(n3).
模式本质上不同于bellman-ford算法。
模型:i->j的最短距离和i->k 加上k->j的最短距离。之间的关系。
靠路径经过的定点的数量来控制循环次数


公式:
dik(k)表示从i->j经过1....k这些点的路径的最小值。当k=0,表示不经过临时点。所以路径就是边的权重。如果k>=1, 那么i->j的最短路径要么经过k, 要么不经过k, 如果不经过k, 那么dij(k)就是dij(k-1), 如果经过k, 那么就是两部分之和: dik(k-1) + dkj(k-1).


那么因为我们这个算法选点选的不是j的前趋点,那么如何构造最短路径呢?
公式:
i->j的通过1.。。。k的最短路径如果过k点,那么j的前趋就是k->j(1.。。k-1)的前趋。如果不过k点。那么i->j(1,...k)的J的前趋就是i->j(1...k-1)的j的前趋。

第一个条件是不过k点,那么i->j(k)代表的前趋应该等于i->j(k-1)代表的前趋。
第二个条件是过k点,那么i->j(k)代表的前趋因该等于k->j(k-1)代表的前趋。

其实,我们既然已经在floyd-warshall算法中知道了选择i->j(k)时的k是设么,我们可以记录下所有的k, 最终我们如果不断更新该路径矩阵,应该也可找到路径。
 15.2.1  Transitive closure
就是一个图的任意定点的联通性的判断。
我们既然可以算出任意定点的最短距离,那么就能知道是否可联通。所以直接使用floyd-warshall算法即可。
此外还有另外算法,既然我们不管新权重。那么
模型:
i->j可联通有如下可能:
1. 有i->j边
2. 有点i->k联通并且k->j联通。检查所有k点。n-1遍循环因该就可找到所有的联通结果。
为了控制循环,可以将ti,j(k)表示成i->j通过1...k作为临时点的联通性。
公式:






算法:
先初始化矩阵。有边的联通为1.
然后n次循环,逐渐扩大联通性。
o(n3)

这里有所有的算法:
http://acm.nudt.edu.cn/~twcourse/TransitiveClosure.html
 15.3  problems
 16  Chapter 26 maximum flow
此问题描述一个图有s,t原点和终点,流体从原点触发,汇入终点。中间的每个点的入的流和出的流一样。每个边的权重代表了该边的capacity. 最大流速。任何两个定点之间的f(u,v)代表了从u->v之间的流量,该数值肯定小于等于u,v之间的capacity.
一个图的流量定义成所有从s流出的流量减去流入s的流量。目前流入流量是0, 所以就是所有从s流出流到t的流量就是这个图的流量。

 16.1  Chapter 26.2 the ford-fulkerson method
此算法仅仅是一个大致的理论方法,并没有具体实现。具体实现可以有很多区别。
词语: residual network Gf, 从一个G和G中的一个flow f可以得到一个residual graph Gf.
Gf定义如下,gf中的点还是g中的点,gf中的边是g中边的流量的余量以及边的流量的逆向。
比如如果u->f 属于G中的边,且f(u,v) = 5, 而c(u,v) = 10. 说明G中,存在u->v的流量5, 而容量是10, 那么对应在gf中就存在从u->v的边,且边的权重是余量10-5=5。 且存在从v->u的边,权重是5,也就是f(u,v)逆向流量。

那么可见,residual graph就是一个基于g和g中已有的流量f,以及各边的capacity算出来的一个可以对原有g作出修改的修改图。那么我们可以在此修改图中找到一个从s到t的path,就意味着,还可一增加s到t的流量。我们算出这个path的流量,将其更新到G中的flow里,然后再算出一个residual graph, 再找有没有新的path......知道不能找到新的path.
这个path叫做augumenting path.
此算法的基本理论:
这里的”augment flow f along p”的意思就是根据我们找到的path p的流量来跟新g中原有的flow的数值。其实就是更新一些边的流量值。
伪码实现:

1-2行初始化,如何从residula network gf中找s到t的path, 可以用dfs,或者bfs. 找到一个path后,该path中权重最小的边即该path的流量。将此流量更新到原图中的流量里。如果原图没有对应的边,意味着,这个新path的边是我们增加进来的逆向边用来减少原图的正向边的流量的。

算法复杂度:
假设权重都是整数,那么假设每次循环都只能让flow增加1,每次循环的复杂度是找一个path的复杂度O(E),总体是O(|f|E)。
《算法导论》(Introduction to Algorithms)是由Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest和Clifford Stein等人合著的一本计算机科学领域经典教材。该书首次出版于1990年,目前已经发行至第三版。 《算法导论》是一本全面讲解算法设计和分析的教材。该书包含了丰富的算法示例和问题实例,在理论和实践上都有很高的指导意义。书中通过对不同算法的介绍,帮助读者理解算法的基本概念、原理和性能分析。 该书分为八个部分,涵盖了算法基础、排序和顺序统计量、数据结构、高级设计和分析技术、高级数据结构、图算法、排序网络和外部存储器模型等内容。每个部分都采用了严谨的语言和清晰的思路来解释算法的原理和应用。 《算法导论》强调了算法设计的重要性,并提供了一种系统的方法来解决各种问题。作者通过讲解不同的算法设计技术,如贪心算法、动态规划和分治算法等,使读者能够理解不同算法之间的差异和适用场景。此外,书中还涉及到算法在计算理论、人工智能和运筹学等领域的应用。 第三版《算法导论》对前两个版本进行了全面的更新和改进。新增加了新的章节和算法示例,反映了计算机科学领域的最新进展。同时,在书中还增加了许多习题和实践案例,以帮助读者更好地理解和掌握算法设计和分析的技巧。 总之,《算法导论》是一本经典的计算机科学教材,它不仅提供了丰富的算法知识和模型分析工具,还能培养读者的算法思维和解决问题的能力。无论是计算机科学专业的学生还是从事算法设计和研究的专业人士,都可以从中获得很大的收益。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值