基础算法记录——由最大子序列和想到的

这是一个入门级的算法,但它却揭示了计算机算法设计的一些核心思想:枚举与分治递归。

这篇文章主要由简单到复杂来解析这一问题,流程大致是:枚举求解(充分利用计算机的计算能力来解决单调复杂问题),算法分析与改进(相对偏移化简枚举法),分治算法(divide-conquer,计算机核心思想之一),递归算法与递归的使用原则,最后依然使用算法分析的技术来改进上面的算法。

1)枚举法,这应该是计算机从业人员最熟悉,最简单的算法。想想你以前写过的那些程序,现在回头再看时,估计自己都会被恶心到。但是当你再过几年code&debug后,我觉得很多人都会对它心生敬畏。枚举法是人类面对浩瀚宇宙,繁杂事物所能做出的最本能和卑微的抵抗。想想你所认知的世界,对知识经验的积累传承,无时无刻都在使用枚举法。

所以,从某种意义上说程序员遇到问题,使用枚举法——这是全人类的本能,无可厚非,甚至应该有所提倡。可惜的是很多人到此为止,不愿意对它刨根问底,改进优化。我想这应该是程序员优秀与否的一个重要标志。

下面来看看这个问题,一个整数序列v[0],...,v[N],找到一个连续的和最大的子序列v[m],..v[m+n].

最简单的方法(应该是最本质的方法)是,我把这个整数序列中所有的子序列都一一列出来,计算每个子序列的和,比较保留最大的那个子序列就可以了。

是的,就应该这么想:下面的问题就是如何实现子序列的选取和求和,这里就有个核心要抓住『连续』。连续就意味着你可以用数组的索引和指针去选取一个个子序列,比较最近两个和,保留最大的那个。

int  maxSum1(vector<int> &v, int & nFrom, int &nTo)

{

int nsum =0;

if(v.size() > 0)

{

    for(int i =0; i<v.size(); i++)

    {

      int vsum =0;

      for(int j=i; j<v.size(); j++)

      {

        for(int k =i; k<=j; k++)

          vsum+=v[k];

        if(nsum< vsum)

        {    nFrom = i, nTo = j;

          nsum = vsum;}

      }

    }

  }

 else

    return 0;

return nsum;

}

下面来进一步的分析一下上面的枚举法的时间复杂度,很简单3个for循环,T(N)=O(N^3)。这里多说几句,一般我们第一次写出来的程序,如果是这个复杂度,那请注意了,这种情况往往有经验可以肯定:这不是最优解。一般可以降到O(N^2),甚至是O(NlogN)。下面就是由里到外的降loop复杂度,这里有个标志就是最里面的循环变量是依赖与外层的循环变量。于是:

int  maxSum1(vector<int> &v, int & nFrom, int &nTo)

{

int nsum =0;

if(v.size() > 0)

{

    for(int i =0; i<v.size(); i++)

    {  int vsum =0;

      for(int j=i; j<v.size(); j++)

      {

        vsum+=v[j];

        if(nsum< vsum)

        {    nFrom = i, nTo = j;

          nsum = vsum;}

      }

    }

  }

 else

    return 0;

return nsum;

}

这时候我们通过扩大计算连续和的范围由原来k:i~j,变到了i~v.size();问题来了,这是否意味着T(N)=O(N^2)是算法的最优解呢。同样要抛弃学院派的理论论证,我们先换个思路来思考:众所周知,O(N^3)>O(N^2)>O(NlogN)>O(N),于是我们猥琐的把目标指向了O(NlogN).一旦看到它,你是否有了点骚动了呢?

2)对!二分法的时间复杂度就是这个!于是我又无耻的想到了是否可以用二分法来将问题一分为二地单独处理其中一个。。。

注意这个时候,我们需要认真分析一下二分法思想(分治算法):问题可以不断的一分为二处理,直到基准情况(此状态必须是已知的)。最后只需要将2种情况合二为一即可得到最优解。

int popMax3(int n1, int n2, int n3)

{

  if(n1>n2)

    if(n1>n3)

      return n1;

    else

      return n3;

  else{

      if(n2 > n3)

        return n2;

      else

        return n3;

    }

}

int  getMaxSubSum(std::vector<int> &v, int &nL, int &nR)

{

  if(nR == nL)

    if(v[nL]>0)

    return  v[nL];

  else

    return 0;

//////////////////////////Divide////////////////////////////////

int nCenter = (nR+nL)/2;

   int nLsum = getMaxSubSum(v, nL, nCenter);//max sum is left of center

int nRsum= getMaxSubSum(v, nCenter, nR);//max sum is right of center

//max sum is at left and right of center

int nHLsum =0, nHRsum = 0 , vsum =0;

  for(int i = nCenter; i >= nL; i--)

  {

    vsum +=v[i];

    if(nHLsum < vsum)

      nHLsum = vsum;

  }

vsum =0;

  for(int j = nCenter+1; j <= nR; j++)

  {

    vsum +=v[j];

    if(nHRsum < vsum)

      nHRsum = vsum;

  }

///////////////////////Conquer////////////////////////////

return  popMax3(nLsum, nRsum, nHLsum+nHRsum);

}

上面就是典型的Divide&Conquer算法的结构,下面需要对其进行时间复杂度分析,很简单:T(N) = T(L)+T(R)+N/2+N/2;于是可以得到:T(N)=2T(N/2)+N;

接下来,数学帝们估计就开始显摆了,其实你再仔细看看,或者干脆把T换成S,明白了吧——这是高中数学的问题了。设N = 2^k,k表示我们需要将一个问题divide几次才能分到基准情况(即nR==nL),于是T(N)= NlogN+N = O(NlogN);写到这里,我想很多人觉得这个问题结束了,或者还有一部分人觉得还有O(N)的解法,但是,我要在这个时候引出另一个问题,上面的算法你觉得最核心的是什么?分治,是其一吧。还有呢?

3)递归

是的,递归。那么问题来了?一堆屌丝大叫到,为嘛用『递归』?难道只要可以分解为类似求解方式的问题(比如2分问题,比如斐波那契问题),都可以用递归吗?

如果是,递归不是要逆天吗?(这一句,大神当我没说!)

下面来一一回答:

首先要负责任的解释一下所谓的递归:递进归纳。一个大的问题可以归纳为一些较小的问题,而较小问题的解是可以假设验证是正确的,当反向追溯问题的解释,其实就是在递进寻找问题的本源。如果你学过卷积算法,认为卷积是系统响应的积累结果,那递归则是,你拿着这个积累结果,按照一个规范(可以理解为一个数学公式)去追溯引起积累结果的本源(类似激起涟漪的石块)。

也许你糊涂了,不过没关系,你要的是如何正确使用?很简单,如果一个问题按照一个规则(不管它多么复杂,例如汉诺塔问题,8皇后问题等等。。。)不断演进,只要你知道它最原始的情况或状态,你就可以大胆的使用『递归』。

但是请不要得意忘形,因为递归也有个使用指南。这个指南的伟大意义在于它直接关系到程序设计的成功与否:

递归四原则:

1.基准情形,要已知。

2.推进方向,向基准。

3.每层递归,可运行。

4.合成效益,要独立,少交叉。

这里只解释一下第三,四2点,其实这就一个核心意思:递归的每层要一致,不能有多种情况。当递归每层时,每个递归层最好不要有交叉。这样会让算法大量的进行重复计算,对时空消耗都是不明智的做法。就好像你计算输入为N的情况,就要递归求N-1和N-2的情况,但是计算N-1的递归时,你又要计算一次N-2的情况。具体的分析和时间复杂度可能都没变化,但是实际随着N的增加,算法的时空消耗往往波动较大。比如斐波那契递归实现,就有这种问题。

OK,这算是说完了该说的重点。下面再继续缩减这个算法的时间复杂度的问题,下文再补上。。。

时间: 2024-10-03 10:09:02

基础算法记录——由最大子序列和想到的的相关文章

基础算法记录

二分查找 lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] def binary_search(lst,item): low = 0 high = len(lst)-1 while low <= high: mid = (low + high)/2 guess = lst[mid] if guess == item: return mid elif guess > item: high =

【基础算法】基础算法【转载】

1. 最大公约数 问题:求两个自然数的最大公约数. 分析:这个是基础的数学问题,最大公约数指两个数字公共的约数中最大的,例如数字6的约数有1.2.3.6,数字9的约数有1.3.9,则数字6和数字9的公共约数有1和3,其中3是最大的公约数. 第一种思路:从1开始循环,每次把符合要求(即同时是两个数字的约数)的值都存储起来,那么最后一个存储起来的就是最大的约数. E:\博客-基础算法-代码\1_a_max_divisor.php: [php] view plain copy <?php /** * 

HDU 3294 Girls&#39; research (Manacher算法 + 记录区间)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3294 题目大意:输入一个字符ch和一个字符串,问如果把ch当作'a'的话,字符串的每个字符也要做相应变化,如b aa,若b为'a',则b前面的a就为'a'前面的'z',这里是循环表示,输出字符串的最长回文子串,如果最长回文子串串长为1,输出No solution! 几乎是模板题,唯一的特别之处就是要输出回文串字符,所以要记录max(Mp[i])对应的在原串中的字符区间,根据Manacher算法的步骤

二叉树基础算法总结

记录一些二叉树的基础算法 二叉树节点结构: typedef struct TreeNode{ int val; struct TreeNode *left; struct TreeNode *right; }TreeNode,*Node; 1.遍历 前.中.后序递归遍历: void pre_order_traversal(TreeNode *root){ if(root! = NULL){ visit(root); pre_order_traversal(root->left); pre_ord

基础算法(一)

首先必须得说本人对算法研究不深,一些简单的就得想半天,老是这样感觉不太好,遂记录下一些常见的基础算法,避免尴尬.不足之处请各位多多指教. 其次,用vs写C语言程序时可能会出现如下错误:错误 C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online hel

常用基础算法C++实现

2016年10月06日10:40:43 本文记录一些常用的基础算法,只为熟能生巧,内容多的话会建立索引的 素数(质数)判断 素数的定义:就是除它本身和1之外,没有其他任何约数的数 1 bool isPrime(int i) 2 { 3 for (int j = 2; j <= sqrt(i * 1.0); j++) { 4 if (i % j == 0) 5 return false; 6 } 7 return true; 8 } 最大公约数 例如:求24和60的最大公约数,先分解质因数,得24

跟着编程之美学算法——最长公共子序列

最长公共子序列是一个很经典的动态规划问题,最近正在学习动态规划,所以拿来这里再整理一下. 这个问题在<算法导论>中作为讲动态规划算法的例题出现. 动态规划,众所周知,第一步就是找子问题,也就是把一个大的问题分解成子问题.这里我们设两个字符串A.B,A = "a0, a1, a2, ..., am-1",B = "b0, b1, b2, ..., bn-1". (1)如果am-1 == bn-1,则当前最长公共子序列为"a0, a1, ...,

基础算法之二——枚举法

基础算法之二--枚举法"赛利的硬币" 题目描述 赛利有 12枚银币.其中有 11枚真币和1枚假币.假币看起来和真币没有区别,但是重量不同.但赛利不知道假币比真币轻还是重.于是他向朋友借了一架天平.朋友希望赛利称三次就能找出假币并且确定假币是轻是重.例如:如果赛利用天平称两枚硬币,发现天平平衡,说明两枚都是真的.如果赛利用一枚真币与另一枚银币比较,发现它比真币轻或重,说明它是假币.经过精心安排每次的称量,赛利保证在称三次后确定假币. 输入数据 输入有三行,每行表示一次称量的结果.赛利事先

c/c++面试总结---c语言基础算法总结2

算法是程序设计的灵魂,好的程序一定是根据合适的算法编程完成的.所有面试过程中重点在考察应聘者基础算法的掌握程度. 上一篇讲解了5中基础的算法,需要在面试之前熟练掌握,本篇讨论剩余的基础算法. 先看一个面试题目:设计一个函数,求一个给定字符串中所有数字的和. 例如:给定字符串 “abc12fas123dfaf34”, 计算结果为:12 + 123 + 34 = 169 其中包括了:求和方法.字符串遍历方法.数字字符转成数字的方法 多位数字组合成整数的方法 必须熟练掌握以上四种基础算法,才能解决该问