《算法导论》第四章_读书笔记

分治思想Divide-and-Conquer

包含三个步骤,分解divide,解决conquer,合并combine

求解递归式的算法效率有三个方法,数学规划,递归树,主方法

最大子数组问题

递归算法

最大子数组的位置只能是三种之一

· 完全位于左子数组

· 完全位于右子数组

· 跨越中点

#include <iostream>

using namespace std;

struct MaximumSubarray{
    int low;
    int high;
    int sum;
};

/******************************
func:求跨越中点的最大子数组
para:A:数组;low:左下标;mid:中下标;high:右下标
******************************/
MaximumSubarray findMaxCrossingSubarr(int A[],int low,int mid,int high){
    int left_sum=(int*)malloc(sizeof(int));//最小负数
    int right_sum=0x80000000;//最小负数
    int max_left=0;
    int max_right=0;
    int sum=0;
    MaximumSubarray maxSubarr={0};

    for(int i=mid;i>=low;--i){
        sum+=A[i];
        if(sum>left_sum){
            left_sum=sum;
            max_left=i;
        }
    }
    sum=0;
    for(int i=mid+1;i<=high;++i){
        sum+=A[i];
        if(sum>right_sum){
            right_sum=sum;
            max_right=i;
        }
    }
    maxSubarr.low=max_left;
    maxSubarr.high=max_right;
    maxSubarr.sum=left_sum+right_sum;
    return maxSubarr;
}

/***********************************
func:求给定数组的最大子数组
para:A:数组;low:左下标;high:右下标
***********************************/
MaximumSubarray findMaxSubarr(int A[],int low,int high){
    MaximumSubarray maxSubarrLeft={0};
    MaximumSubarray maxSubarrRight={0};
    MaximumSubarray maxSubarrCross={0};
    if(high==low){          //特殊情况:只有一个元素
        maxSubarrLeft.low=low;
        maxSubarrLeft.high=high;
        maxSubarrLeft.sum=A[low];
        return maxSubarrLeft;  
    }
    int mid=(low+high)/2;
    maxSubarrLeft=findMaxSubarr(A,low,mid);
    maxSubarrRight=findMaxSubarr(A,mid+1,high);
    maxSubarrCross=findMaxCrossingSubarr(A,low,mid,high);
    if(maxSubarrLeft.sum>maxSubarrRight.sum&&maxSubarrLeft.sum>maxSubarrCross.sum)
        return maxSubarrLeft;
    else if(maxSubarrRight.sum>maxSubarrLeft.sum&& maxSubarrRight.sum>maxSubarrCross.sum)
        return maxSubarrRight;
    else return maxSubarrCross;
}

int main(int argc,char* argv[]){
    int A[16]={13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
    MaximumSubarray resMaxSubarr=findMaxSubarr(A,0,15);
    cout<<"maximum subarray is:"<<endl;
    cout<<"resMaxSubarr.low:"<<resMaxSubarr.low<<endl;
    cout<<"resMaxSubarr.high:"<<resMaxSubarr.high<<endl;
    cout<<"resMaxSubarr.sum:"<<resMaxSubarr.sum<<endl;
    return 0;
}

代码来源

算法性能为 Θ(nlgn) ,优于暴力求解的 Θ(n2)

这里贴出包含计时器的测试函数代码

void test_max_array(void)
{
//  int Data[16]={13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
    int* Data=(int*)malloc(sizeof(int)*LENGTH);
    srand((unsigned)time(NULL)); 
    for(int i=0;i<LENGTH;i++)                      //随机产生随机数
    {
        int flag=rand()%2;
        int temp=rand()%100;   
        if(flag==0)
            Data[i]=0-temp;
        else
            Data[i]=temp;
    }
    printf("\n随机产生100个数:\n");
    for(int i=0;i<LENGTH;i++)                     
    {
        if(i%10==0)
            printf("\n");
        printf("%d  ",Data[i]);
    }    

    int *left=(int*)malloc(sizeof(int));
    int *right=(int*)malloc(sizeof(int));

    int start_time=GetTickCount();   //记录开始时间

    int sum=Find_maximum_subarray(Data, 0, LENGTH-1, left, right);  
    printf("\n\n分治求解答案:\nleft=%d\nright=%d\nsum=%d\n\n",*left,*right,sum);

    Time_calculate(start_time);       //打印结束时间
    start_time=GetTickCount();   //记录开始时间

    int sum_force=Force_search(Data, 0, LENGTH-1, left, right);
    printf("\n暴力求解答案:\nleft=%d\nright=%d\nsum_force=%d\n\n\n\n",*left,*right,sum_force);

    Time_calculate(start_time);     //打印结束时间。

    free(left);
    free(right);
}

来源

非递归的线性问题的算法(动态规划)


动态规划思想:是由上一阶段的问题的解(可能被分解为多个子问题)递推求解出最终问题的解。

动态规划问题包含两个部分,一是问题的状态,二是状态转移方程

该问题的算法思路:

从数组的左边界开始,由左至右处理,记录到目前为止已经处理过的最大子数组。若已知A[1,j]的最大子数组,基于如下性质将解扩展为A[1,j+1]的最大子数组:

A[0,j+1]的最大子数组,要么是

1)某个子数组A[i,j+1],(0≤i≤j+1),

2)就是A[1,j]的最大子数组

在已知A[1,j]的最大子数组的情况下,可以在线性时间内找出形如A[i,j+1]的最大子数组。

//寻找A[i,j+1]中最大子数组,子数组右下标已经固定
MaximumSubarray FIND_MAX_ARRAY(int A[],int low,int high){
    MaximumSubarray maxSubarr={0};
    if (high==low){
        maxSubarr.low=low;
        maxSubarr.high=high;
        maxSubarr.sum=A[low];
        return maxSubarr;
    }
    maxSubarr.low=high;
    maxSubarr.high=high;
    maxSubarr.sum=A[high];
    int tmp_sum=A[high];
    for (int i=high-1;i>=low;--i){
         tmp_sum+=A[i];
         if(tmp_sum>maxSubarr.sum){
             maxSubarr.sum=tmp_sum;
             maxSubarr.low=i;
         }
    }
    return maxSubarr;
}

/***********************************************
func:求给定数组的最大子数组
para:A:数组;low:起始下标;high:结束下标
***********************************************/
MaximumSubarray FIND_MAXIMUM_SUBARRAY(int A[],int low,int high){
    MaximumSubarray maxSubarr={0};
    if (high==low){
        maxSubarr.low=low;
        maxSubarr.high=high;
        maxSubarr.sum=A[low];
        return maxSubarr;
    }
//从A[0]开始
    maxSubarr.low=low;
    maxSubarr.high=low;
    maxSubarr.sum=A[low];
    MaximumSubarray tmpSubarr={0};

    for (int i=low+1; i<=high;++i)
        tmpSubarr=FIND_MAX_ARRAY(A,low,i);
  //如果A[i,j+1]中最大子数组大于A[1,j]的最大子数组
        if(tmpSubarr.sum>maxSubarr.sum)
            maxSubarr=tmpSubarr;
    return maxSubarr;
}

int main(int argc,char* argv[]){
    int A[16]={13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
    MaximumSubarray resMaxSubarr=FIND_MAXIMUM_SUBARRAY(A,0,15);
    cout<<"dynamic programming maximum subarray is:"<<endl;
    cout<<"resMaxSubarr.low:"<<resMaxSubarr.low<<endl;
    cout<<"resMaxSubarr.high:"<<resMaxSubarr.high<<endl;
    cout<<"resMaxSubarr.sum:"<<resMaxSubarr.sum<<endl;
    return 0;
}

代码来源

其实其中有很多可优化空间

比如判断当前元素A[j+1]是否为正:

​ 如果为负值,则情况1)不用分析;

​ 如果为正,则计算情况1);

比如判断当前最大子元素是否为正

如果为负,而当前元素A[j+1]为正,则情况2)不用分析,且计算情况1)时,应该从前一个最大子数组的最右下标往后的第一个正数位置开始考虑

而如果想达到线性时间的性能,则需要通过缓存一些中间结果来实现

具体参考博客

ps clrs.skanev.com 的算法好工整啊 ,贴出来

We need to build an array S that holds the maximum subarrays ending on each index of A. That is, S[j] holds information about the maximum subarray ending on j.

We first loop through the input to build S. Afterwards, we do what they suggest in the text. This is n+n=2n=Θ(n).

typedef struct {
    unsigned left;
    unsigned right;
    int sum;
} max_subarray;

max_subarray find_maximum_subarray(int A[], unsigned low, unsigned high) {
//计算最右下标固定的最大子数组
    max_subarray suffixes[high - low];

    suffixes[0].left = low;
    suffixes[0].right = low + 1;
    suffixes[0].sum = A[low];

    for (int i = low + 1; i < high; i++) {
    //如果上一个最大子数组是个负数
        if (suffixes[i - 1].sum < 0) {
            suffixes[i].left = i;
            suffixes[i].right = i + 1;
            suffixes[i].sum = A[i];
        } else {
        //如果s[i]大于零,那s[i+1]就是直接把A[i]加进去

            max_subarray *previous = &suffixes[i - 1];
            suffixes[i].left = previous->left;
            suffixes[i].right = i + 1;
            suffixes[i].sum = previous->sum + A[i];
        }

    }

    max_subarray *max = &suffixes[0];

    for (int i = low + 1; i < high; i++) {
        if (max->sum < suffixes[i].sum) {
            max = &suffixes[i];
        }
    }

    return *max;
}

主定理

习题 4.5.5 题目

考虑在某个常数c < 1时的规则性条件af(n/b)<=cf(n),此条件是主定理第三种情况的一部分。举一个常数a >= 1,b > 1以及一个函数f(n),满足主定理第三种情况中的除了规则性条件之外的所有条件的例子。

分析与解答

首先f(n)要比 nlogba 大,若 nlogba 是多项式函数,那么f(n)至少是多项式函数;

同时要满足f(n)是多项式大,如果满足了多项式大,那么如果f(n)是多项式函数的话,很容易证明,由于f(n)的最高次比 nlogba 大,所以af(n/b)最高次的系数a/( blogba + ε ) < 1,所以规则性条件肯定成立,因此f(n)的增长性要比多项式函数大,但是增长性快的函数,f(n/b)影响是决定性的,不论a多大其都会在n足够大时小于cf(n);

所以考虑使用两类函数进行复合组成f(n),例如将两类函数相除,分母函数的增长性大于分子函数的增长性,考虑多项式函数与对数函数相除,这样f(n/b)可以还原出组成组成f(n)的函数,但是如果多项式函数的次数大于1的话,还是可以找到满足规则性条件,因为将这类复合函数带入规则性条件,最终都可以得到(mc - 1)lgn >= mc的形式,如果组成f(n)的多项式函数的次数大于1的话,m就会大于1,可以找到c使(mc-1)大于0,此时必然可以找到n使(mc - 1)lgn>= mc成立。

可见如果是用多项式函数与对数函数相除组成f(n)的话, nlogba 得是常数,即a=1,此时因为c< 1,所以c-1小于0,那么不等式(mc - 1)lgn >= mc不可能成立,即递归式就不满足规则性条件。

例如递归式: T(n)=T(n/2)+n2/lgn

来源

另外clrs.skanev给出的解答 是

T(n) = T(n/2) + n(2−cosn)

思考题4.2 参数传递的代价

意思就是在原递归式的基础上加上一个参数传递代价,重新计算渐近紧确界

standford algorithm on coursera week 1 note

building machine learning systems with python

机器学习系统设计

refuse to be content

所有算法都还有优化空间

永远要问自己

芯片检测问题

问题:Diogenes 教授有n个被认为是完全相同的VLSI芯片,原则上它们是可以互相测试的.教授的测试装置一次可测试二片,当该装置中放有两片芯片时,每一片就对另一片作 测试并报告其好坏.一个好的芯片总能够正确的报告另一片的好坏,但一个坏的芯片的结果就是不可靠的

a)证明若少于 n/2 的芯片是坏的,在这种成对测试方式下,使用任何策略都不能确定哪个芯片是好的.
b)假设有多于 n/2 的芯片是好的,考虑从 n 片中找出一片好芯片的问题.证明 n/2 对测试就足以使问题的规模降至近原来的一半.
c)假设有多于 n/2 的芯片是好的,证明好的芯片可用 O(n) 对测试找出。

思路:

两两配对进行检测(检测次数为n/2下界),那么结果中有

a个oo b个xx c个ox

假设剩下1个

那么a>b+1

我们选出那些结果为1芯片,

因为a>b,所以用这a+b个芯片去检测最后那个零头,那么如果超过一半说是坏的,那么最后能检测出正确结果

附录B

假设R是非空集合A中的一个关系,并且具有对称性和传递性。有人断定R是一个等价关系,其推理如下:
“对a,b∈A,从a R b得b R a,又从传递性得a R a,因而R有自反性,故为等价关系,”(题目问他的推理对吗?)

这个推论是错误的

解答:

由对称性: 如果存在aRb,那么推出 bRa
但自反性的定义是: 任意a, aRa. 这里推不出自反性

神举例:

A能和B接吻 B能和C接吻 那么 A能和C接吻 传递;
A能和B接吻 B能和A接吻 对称;
A能吻他自己否??????

附录C

c 2.6 描述一个以整数a和b为输入的过程,其中0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值