算法总结:动态规划学习笔记

在动态规划中,状态与状态转移方程以及初始值是最重要的,只有给出了状态和状态转移方程,一切就好办了。

Fibonacci

要求输入一个整数n,请输出fibonacci数列的第n项
我们一般的方法是使用递归的方法,但是它的时间复杂度是O(2^n),当n这个数字很大时,效率会很低,甚至栈溢出,此时我们就可以使用动态规划的方法来解决这个问题;

首先,我们给出这个问题的状态:求F(n)
状态转移方程是:F(n)=F(n-1)+F(n-1);
初始值为:F(1)=F(2)=1;
返回结果是:F(n);

此时我们可以实现代码:

class Solution {
public:
    int Fibonacci(int n) {
        if(n<=0)
            return 0;
        if(n==1 || n==2)
            return 1;
        int* arr=new int[n+1];
        arr[0]=0;
        arr[1]=1;
        for(int i=2;i<=n;++i)
        {
            //状态转移方程是F(n)=F(n-1)+F(n-2)
            arr[i]=arr[i-1]+arr[i-2];
        }
        return arr[n];
        delete[] arr;
    }
};

上面这种方法的空间复杂度是O(n),其实F(n)只与他的前两项相关,没必要保存所有子问题的解,可以使用变量来保存每一次的结果,例如:

class Solution {
public:
    int Fibonacci(int n) {
        if(n<=0)
            return 0;
        if(n==1 || n==2)
            return 1;
        int c=0;
        int a=0;
        int b=1;
        for(int i=2;i<=n;++i)
        {
            c=a+b;
            a=b;
            b=c;
        }
        return c;
    }
};

这种方法的空间复杂度就是O(1);

青蛙跳台阶

一只青蛙一次可以跳一级台阶,也可以跳两级台阶,求该青蛙跳上n阶台阶总共有多少种跳法
我们可以分析一下,它的状态是F(n),
n=0时,F(n)=0;n=1时,F(n)=1;n=2时,F(n)=2;而当n>2时,第一次跳可以分为两种情况,第一种是第一次只跳一级,此时它的跳法数为剩下的台阶跳数,即F(n-1);第二种情况是第一次跳两级,此时为F(n-2);则最终的状态转移方程为F(n)=F(n-1)+F(n-2),实现代码与斐波那契数列相似

变态青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
首先我们来分析一下这个问题
它的状态是F(n),它的子状态是跳上1级、2级、3级…n级台阶的跳法数,具体分析一下:

当n=0时,F(n)=0;
当n=1时,F(n)=1;
当n=2时,F(n)=2;
当n>2时,此时我们要分为n种情况,即:
(1)第一次跳一级,则他的跳法数为F(n-1);(2)第一次跳两级,则它的跳法数是F(n-2);(3)当第一次跳三级,则它的跳法数是F(n-3)。。。。。当第一次跳n级时,则它的跳法数是F(n-n)=F(0);

则最终的转移方程就是F(n)=F(n-1)+F(n-2)+F(n-3)+…+F(0),但是此时我们可以化简,F(n-1)=F(n-2)+F(n-3)+…+F(0),则F(n)-F(n-1)=F(n-1),则F(n)=2*F(n-1);
实现代码如下:

class Solution {
public:
    int jumpFloorII(int number) {
        if(number<=0)
            return 0;
        int ret=1;
        for(int i=1;i<number;++i)
        {
            ret*=2;
        }
        return ret;
    }
};

但是这种方法的时间复杂度是O(n),我们可以降低时间复杂度,使用移位操作,由于是等比数列,可以直接使用,例如:

class Solution {
public:
    int jumpFloorII(int number) {
        if(number<=0)
            return 0;
        return 1<<(number-1);
    }
};

扩展1:如果此时我们限制青蛙一次只能跳一级或者两级,求该青蛙跳上一个n级的台阶总共有多少种跳法。
我们继续分析,F(1)=1,F(2)=1+1(一次跳两级和一级一级跳两种跳法),F(3)=1+2(一级一级跳,一次跳两级然后跳一级和一次跳一级然后跳两级,共三种跳法),F(4)=1+1+1+1+1(一级一级跳,先跳两级再跳两级,先跳两级再跳一级再跳一级,先跳一级再跳两级再跳一级,先跳一级再跳一级再跳两级,共五种跳法)
我们这里可以分析出当前级的跳法数与前面一级和前面一级的前面一级相关(因为只能跳一级或者两级),所以当前级=前面一级+前面一级的前面一级,即F(i)=F(i-1)+F(i-2),即斐波那契数列;
扩展2:矩形覆盖:我们可以用21的小矩形横着或者竖着去覆盖更大的矩形,请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
我们也是按照上面的方法来进行分析,它的状态是F(n),
F(1)=1(只能横着覆盖,一种)
F(2)=2(可以横着和竖着覆盖,两种)
F(3)=1+2(先竖着放一个后面还有3-1个位置,先横着放一个,后面只能横着放1个,这时后面只能有3-2个位置可以放)
此时可以得出递推方程是F(i)=F(i-1)+F(i-2),这也是一个斐波那契数列问题;

最大连续子数组和

它的状态就是F(i)=以array[i]为末尾元素的子数组和的最大值
状态递推方程:F(i)=(F(i-1)>0) ? F(i-1)+array[i] : array[i];
返回值:所有F(i)中的最大值maxvalue
实现代码:

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) { 
        if(array.empty())
            return -1;
        //初始值
        int sum=array[0];
        int maxvalue=array[0];
        for(int i=1;i<array.size();++i)
        {
            //状态转移方程:F(i)=(F(i)>0) ? F(i)+array[i] : array[i]
            sum=(sum>0) ? sum+array[i] : array[i];
            //返回值
            maxvalue=(maxvalue>sum)? maxvalue:sum;
        }
        return maxvalue;
    }
};
字符串分割

给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“leetcode”;
dict=[“leet”, “code”].
返回true,因为"leetcode"可以被分割成"leet code".
首先我们来分析一下这个题目的状态,它的子状态是前1、2、3…i个字符可不可以被成功分割成单词,即F(i):前i个字符是否能够成功分割成单词;
以s=“leetcode”来举例,

由于并不知道初始状态是什么,先给一个辅助状态F(0)=true;
F(1)=“l”,F(0) && "l"不能在dict中找到,false;
F(2)=“le”,F(0) && "le"不能在dict中找到,false;
F(3)=“lee”,F(0) && "lee"不能在dict中找到,false;
F(4)=“leet”,F(0) && "leet"能在dict中找到,true;
F(5)=“leetc”,F(4) && "c"不能在dict中找到,false;
F(6)=“leetco”,F(4) && "co"不能在dict中找到,false;
F(7)=“leetcod”,F(4) && "cod"不能在dict中找到,false;
F(8)=“leetcode”,F(4) && "code"能在dict中找到,true;

可以看出每一个子状态都与上一个子状态有关,
它的初始值不确定,此时我们要引入一个辅助状态让F(0)=true,
它的状态递归方程是:F(i):只要F(j)存在并且j<i且substr(j,i-j)能够在词典中找到,为true,否则为false,实现代码是:

class Solution {
public:
    bool wordBreak(string s, unordered_set<string> &dict) {
        if(s.empty())
            return false;
        if(dict.empty())
            return false;
        vector<bool> F(s.size()+1);
        F[0]=true;
        for(int i=1;i<=s.size();++i)
        {
            for(int j=0;j<i;++j)
            {
                if(F[j])
                {
                    if(dict.find(s.substr(j,i-j))!=dict.end())
                    {
                        F[i]=true;
                        break;
                    }
                }
            }
        }
        return F[s.size()];
    }
};

分析上述代码:给一个存放每一个分词为true或者false的顺序表F,初始状态为true,使用两个循环,当F(j)为true时,分隔从j开始长度为i-j的字符串,如果可以在dict中找到并且不在dict的末尾,让F(i)置为true,跳出当前循环;
例如s=“leetcode”,

第1次j在l处,i在e处,F[j]为true(辅助状态),判断从j开始分隔i-j长度的字符串l不在dict中,第二次j=1与i在一处,不执行;

第2次i走到第二个e处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串le在dict中找不到,然后继续循环,j=1,F[j]=false,不用判断;

第3次i走到t处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串lee在dict中找不到,继续循环,j走到e处,此时F[j]为false,不用判断,继续循环,j处在e处,此时F[j]为false,不用判断,继续循环,j处在t处,与i在一处不用循环;

第4次i走到c处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串leet在dict中可以找到,F[i](F(4))置为true,结束当前循环;

第5次i走到o处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串leetc在dict中找不到,继续循环,j处在e处,此时F[j]为false,不用进去判断,继续循环,j处在第二个e处,此时F[j]为false,不用进去判断,继续循环,j处在t处,此时F[j]为false,不用进去判断,继续循环,j处在c处,此时F[j]为true,进去判断,分隔的字符串c在dict中找不到,继续循环,j处在与i一样的位置,不用执行循环;

第6次i走到d处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串leetco在dict中找不到,继续循环,j处在e处,此时F[j]为false,不用进去判断,继续循环,j处在第二个e处,此时F[j]为false,不用进去判断,继续循环,j处在t处,此时F[j]为false,不用进去判断,继续循环,j处在c处,此时F[j]为true,进去判断,分隔的字符串co在dict中找不到,继续循环,j处在o处,此时F[j]为false,不用进去判断,继续循环,j处在与i相同的位置,不用循环;

第7次i走到e处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串leetcod在dict中找不到,继续循环,j处在e处,此时F[j]为false,不用进去判断,继续循环,j处在第二个e处,此时F[j]为false,不用进去判断,继续循环,j处在t处,此时F[j]为false,不用进去判断,继续循环,j处在c处,此时F[j]为true,进去判断,分隔的字符串cod在dict中找不到,继续循环,j处在o处,此时F[j]为false,不用进去判断,继续循环,j处在d处,此时F[j]为false,不用进去判断,继续循环,j处在与i一样的位置,不用循环;

第8次i走到size处,j从l开始,此时F[j](F(0))为true,进去判断,分隔的字符串leetcode在dict中可以找到,并且不在dict的末尾,此时循环条件结束,返回F[s.size()];

三角矩阵

给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字,
例如,给出的三角形如下:
[↵ [2],↵ [3,4],↵ [6,5,7],↵ [4,1,8,3]↵]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
我们先来分析一下,使用动态规划的方法做,它的状态是从(0,0)到(n,n)的最短路径和,那么只要保证它的每一个子状态,即从(0,0)到(1,0),(1,1),(2,0),…(n,n)的最短路径和,只要每一层都能得到最短路径,那么最后的状态也是最小的,即它的状态递推方程是:F(i,j)=min(F(i-1,j),F(i-1,j-1))+triangle[i,j],(即当前这个数的上一层的最短路径加上自己的值),但是我们要特别注意,第一列的路径和只能是F(i-1,j)+triangle[i,j],最后一列的路径和只能是F(i-1,j-1)+triangle[i,j],当加到最后一行时,比较最后一行每一列得到的路径和,取最小的即可,实现代码:

class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
        if (triangle.empty())
        {
             return 0;
        }
        vector<vector<int>> min_sum(triangle);
         int line = triangle.size();
         for (int i = 1; i < line; i++)
         {
             for (int j = 0; j <= i; j++)
             {
                 if (j == 0)
                 {
                     min_sum[i][j] = min_sum[i - 1][j];
                 }
                 else if (j == i){
                     min_sum[i][j] = min_sum[i - 1][j - 1];
                 }
                 else{
                     min_sum[i][j] = min(min_sum[i - 1][j], min_sum[i - 1][j - 1]);
                 }
				 // F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j]
                 min_sum[i][j] = min_sum[i][j] + triangle[i][j];
             }
         }
         int result = min_sum[line - 1][0];
        // min(F(n-1, i))
         for (int i = 1; i < line; i++){
             result = min(min_sum[line - 1][i], result);
         }
         return result;
     }
};

但是这个代码的时间复杂度为比较高,好费时间,那么我们可不可以进行优化呢,接下来我们可以分析一下,我们可以使用反向思维,即它的子状态是:从(n,n),(n,n-1),…(1,0),(1,1),(0,0)到最后一行的最短路径和,即它的状态是F(i,j)到最后一行的路径和,它的状态递归方程就是:F(i,j)=min(F(i+1,j),F(i+1,j+1))+triangle(i,j);它的初始值是F(n-1,0)=triangle[ n-1,0], F(n-1,1) = triangle[n-1][1],…, F(n-1,n-1) = triangle[n-
1][n-1];它的返回值是F(0,0),这种逆向的思维不需要最后寻找最小值,也不需要考虑边界,实现代码为:

class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
       if (triangle.empty())
       {
         return 0;
       }
       vector<vector<int>> min_sum(triangle);
       int line = triangle.size();
       for (int i = line - 2; i >= 0; i--)
       {
           for (int j = 0; j <= i; j++)
           {
 // F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
               min_sum[i][j] = min(min_sum[i + 1][j], min_sum[i + 1][j + 1]) +
triangle[i][j];
            }
       }
       return min_sum[0][0];
    }
};
路径总数

题目描述为 在一个m*n的网格的左上角有一个机器人,机器人在任何时候只能向下或者向右移动,机器人试图到达网格的右下角,有多少可能的路径。
首先我们来分析这个题目的状态,它的状态是从F(0,0)到F(i,j)的路径总数,那么它的子状态是从(0,0)到(1,0),(1,1),(2,1)…(m-1,n-1)的路径总数,由于只能向下或者向右走,他每到达一个位置的路径总数都与上一个到达的地方的路径总数相关,它的状态递推方程是F(i,j)=F(i-1,j)+F(i,j-1),它的初始值为即特殊情况是第0行和第0列,即F(0,i)=1,F(i,0)=1,返回结果是F(m-1,n-1),实现代码为:

class Solution {
public:
    int uniquePaths(int m, int n) {
        if(m<1 || n<1)
            return 0;
        vector<vector<int>> arr(m,vector<int>(n,1));
        for(int i=1;i<m;++i)
        {
            for(int j=1;j<n;++j)
            {
                arr[i][j]=arr[i][j-1]+arr[i-1][j];
            }
        }
        return arr[m-1][n-1];
    }
};

扩展: 和第六题的框架相同,机器人还是要从网格左上角到达右下角,
但是网格中添加了障碍物,障碍物用1表示
此时我们分析一下,只要第一列或者第一行有障碍物,那么后面的就都没有办法到达,这是两种特殊情况,开始走,如果遇到障碍物,此时就将F(i,j)置为0,说明这里不能走,路径总数为0,其余与上述一样,实现代码:

class Solution {
public:
  int uniquePathsWithObstacles(vector<vector<int> > &obstacleGrid) {
      if(obstacleGrid.empty() || obstacleGrid[0].empty())
          return 0;
      int row=obstacleGrid.size();
      int col=obstacleGrid[0].size();
      vector<vector<int>> ret(row,vector<int>(col,0));
      for(int i=0;i<row;++i)//考虑第一列的情况
      {
          if(obstacleGrid[i][0]==1)
              break;
          else
              ret[i][0]=1;
      }
      for(int i=0;i<col;++i)//考虑第一行的情况
      {
          if(obstacleGrid[0][i]==1)
              break;
          else
              ret[0][i]=1;
      }
      for(int i=1;i<row;++i)
      {
          for(int j=1;j<col;++j)
          {
              if(obstacleGrid[i][j]==1)
                  ret[i][j]=0;
              else
                  ret[i][j]=ret[i][j-1]+ret[i-1][j];
          }
      }
      return ret[row-1][col-1];
  }
};
最小路径和

给定一个m*n的网格,网格用非负数填充,找到一条从左上角到右下角的最短路径,注:每次只能向下或者向右移动。
我们使用动态规划的方法,这个题与之前的三角矩阵的问题有点相似,结合路径计算方式,它的状态是从F(0,0)到F(i,j)的最小路径,子状态就是从(0,0)到(1,0),(1,1),(2,1)…(m-1,n-1)的最小路径,我们得到它的状态递推方程就是F(i,j)=min(F(i,j-1),F(i-1,j))+grid(i,j),它的特殊情况是第0列和第0行,F(0,i)=F(0,i-1)+grid(0,i),F(i,0)=F(i-1,0)+grid(i,0),初始值为F(0,0)=(0,0),实现代码为:

class Solution {
public:
   int minPathSum(vector<vector<int> > &grid) {
       if(grid.empty() || grid[0].empty())
           return 0;
       int m=grid.size();
       int n=grid[0].size();
       vector<vector<int>> ret(m,vector<int>(n,0));
       ret[0][0]=grid[0][0];
       for(int i=1;i<m;++i)//第一列
       {
           ret[i][0]=ret[i-1][0]+grid[i][0];
       }
       for(int i=1;i<n;++i)
       {
           ret[0][i]=ret[0][i-1]+grid[0][i];
       }
       for(int i=1;i<m;++i)
       {
           for(int j=1;j<n;++j)
           {
               ret[i][j]=min(ret[i-1][j],ret[i][j-1])+grid[i][j];
           }
       }
       return ret[m-1][n-1];
   }
};
背包问题

有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值,问最多能装入背包的总价值是多大?
这个是动态规划的经典问题,首先我们来分析状态,用F(i,j)表示前i个物品放入大小为j的背包中所获得的最大价值,我们可以分析出,对于第i个物品,我们要考虑放不放的下,如果放不下,即第i个物品放不下(A[i-1]>j),此时只能从前i-1个物品中去做选择,此时的价值就是前i-1个物品的价值,即F(i,j)=F(i-1,j);
如果第i个物品可以放的下,我们可以选择放或者不放,因为可能放了价值还会减小;
如果我们此时放第i个物品,那么我们可以得知此时背包中剩余空间是j-A[i-1],此时我们的总价值就是F(i-1, j - A[i]) + V[i]:表示把第i个物品放入背包中,价值增加V[i],但是需要腾出j - A[i]的大小放第i个商品;
如果我们选择不放第i个物品,此时总价值就是F(i-1,j): 表示不把第i个物品放入背包中, 所以它的价值就是前i-1个物品放入大小为j的背包的最大价值,
我们要在放和不放中选择总价值最大的,即F(i, j) = max{F(i-1,j), F(i-1, j - A[i]) + V[i]},它的初始值是F(i,0)=F(0,j)=0,表示没有装入物品时价值为0;
例如 在这里插入图片描述
我们来分析一下:
背包大小m=14;
在这里插入图片描述
对于第一个A物品,不管背包大小是多少,它的价值都是1,因为A物品的价值是1;
对于第二个B物品,当背包大小小于B物品的大小10时,他都只能放得下A物品,价值为1,当背包大小为10时,可以放下B物品,放下后剩余大小是0,价值为10,不放的话价值是1,因此放入B物品,价值为10,当背包大小是11时,A物品和B物品都放进去,价值为11,后面的价值都是11;
对于第三个C物品,当背包大小小于C物品的大小5时,他都放不进去,都只能放下A物品,因此价值都是1,当背包大小为5时,可以放的下C物品,如果放进去,背包的价值就是2,不放价值就是1,因此放进去,当背包大小是6时,可以放进去C物品,但是还可以放进去A物品,价值为3,当背包大小小于B物品的大小10时,B物品都放不进去,价值都为3,当背包大小为10时,B物品可以放进去,放进去价值为10,不放价值为3,因此放进去,当背包大小是11时,B无物品可以放进去,但是还留一个大小为1的位置,可以放的下A物品,放进去A物品价值为11,不放价值为10,因此放进去,后面的价值都是11;
对于第四个D物品,当背包大小小于D物品的大小2时, D物品都放不进去,当背包大小为2时,可以放下D物品,此时放进去价值为5,不放价值为1,因此放进去,当背包大小为3时,放进去D物品,还剩下1个大小的空间,可以放下A物品,放A物品价值为6,不放价值为5,因此放进去,以此类推我们可以得到上图,最终的最大价值为18,
实现代码:

class Solution {
public:
 int backPackII(int m, vector<int> A, vector<int> V) {
 if (A.empty() || V.empty() || m < 1)
 {
 	return 0;
 }
 //多加一行一列,用于设置初始条件
 const int N = A.size() + 1;
 const int M = m + 1;
 vector<vector<int> > result;
 result.resize(N);//为最终的结果二维数组的每一列分配内存
 //初始化所有位置为0,第一行和第一列都为0,初始条件
 for (int i = 0; i < N; ++i) 
 {
 	result[i].resize(M, 0);
 }
 for (int i = 1; i < N; ++i) 
 {
	 for (int j = 1; j < M; ++j) 
	 {
	 //第i个商品在A中对应的索引为i-1: i从1开始
	 //如果第i个商品大于j,说明放不下, 所以(i,j)的最大价值和(i-1,j)相同
	 if (A[i - 1] > j) 
	 {
		 result[i][j] = result[i - 1][j];
	 }
 	//如果可以装下,分两种情况,装或者不装
 	//如果不装,则即为(i-1, j)
 	//如果装,需要腾出放第i个物品大小的空间: j - A[i-1],装入之后的最大价值即为(i - 1, j - A[i-1]) + 第i个商品的价值V[i - 1]
 	//最后在装与不装中选出最大的价值
	 else 
	 {
 	int newValue = result[i - 1][j - A[i - 1]] + V[i - 1];
 	result[i][j] = max(newValue, result[i - 1][j]);
 	 }
	}
 }
 //返回装入前N个商品,物品大小为m的最大价值
 return result[N - 1][m];
 }
};

由于以上第i行的计算结果只与i-1行有关,因此可以优化为一维数组,但是如果是一维向量,需要从后向前计算,因为后面的元素更新需要依靠前面的元素未更新(模拟二维矩阵的上一行的值)的值,实现代码:

int backPackII2(int m, vector<int> A, vector<int> V)
{
	if (A.empty() || V.empty() || m < 1)
		return 0;
	vector<int> result;
	int N = A.size();
	int M = m + 1;
	result.resize(M, 0);
	//这里的i-1理解为上一行,因此从0开始
	for (int i = 0; i < N; ++i)//i表示每一行
	{
		for (int j = M - 1; j > 0; --j)//j表示遍历当前行的每一列
		{
			if (A[i] > j)
				result[j] = result[j];
			else
			{
				int tmp = result[j - A[i]] + V[i];
				result[j] = max(tmp, result[j]);
			}
		}
	}
	return result[m];
}
回文串分割

给定一个字符串 s,把 s 分割成一系列的子串,分割的每一个子串都为回文串,返回最小的分割次数;比如,给定 s = “aab”,返回1,因为一次cut就可以产生回文分割[“aa”,“b”]
我们可以得出它的状态是F(i):到第i个字符的最小分割次数,我们可以得出它的子状态是:到第1,2,3,…,n个字符需要的最小分割数 ,接下来要求出状态转移方程,我们可以得出(1到i),(1到i-1,i),(1到i-2,i-1到i)…(1,2到i)这几种分割方式,因此当j<i并且[1,j]字符的最小分割数我们已经知道并且[j+1,i]是回文串,即j<i && [j+1,i]是回文串 && min(F(i),F(j)+1),j可能取任何值,此时就可以保证从第一个字符到第j个字符是回文串,从j+1到i也是回文串,并且1到j的最小切割数已经知道,也就是F(j)=F(j)+1;
返回F(length),实现代码:

class Solution {
public:
   bool isPal(string& s,int start,int end)
   {
       while(start<end)
       {
           if(s[start]!=s[end])
           {
               return false;
           }
           start++;
           end--;
       }
       return true;
   }
   int minCut(string s) {
       int len=s.size();
       vector<int> minSum(len+1,0);
       for(int i=1;i<=len;++i)
       {
           minSum[i]=i-1;//i个字符的最大分割次数为i-1
       }
       for(int i=1;i<=len;++i)
       {
           for(int j=0;j<i;++j)
           {
               if(isPal(s,0,i-1))//整体是回文串,先将是回文串的部分摘出来
               {
                   minSum[i]=0;
                   break;
               }
               if(isPal(s,j,i-1))
               {
                   minSum[i]=min(minSum[i],minSum[j]+1);
               }
           }
       }
       return minSum[len];
   }
};

但是上述方法的时间复杂度为O(n^3),我们可以将判断是否为回文串的函数进行优化,它也是一个是或不是的问题,我们可以使用动态规划的方法来做,它的状态是F(i,j):字符区间[i,j]是否为回文串,它的子状态是从第一个字符到第二个字符是不是回文串,第1-3,第2-5,…,来分析一下,如果字符串首尾字符相同且在去掉字符区间首尾字符后剩下的也是回文串,那么原字符区间是回文串,因此它的状态递推方程是F(i,j):s[i]==s[j]&&F(i+1,j-1),返回true,否则返回false,
初始化F(i,j)=false,返回结果是矩阵F(n,n), 只更新一半值(i <= j),n^2 / 2,实现代码:

int minCut(string s) 
{
	if (s.empty()) 
		return 0;
	int len = s.size();
	vector<int> cut;
	// F(i)初始化
	// F(0)= -1,必要项,如果没有这一项,对于重叠字符串“aaaaa”会产生错误的结果
	for (int i = 0; i < 1 + len; ++i) 
	{
		cut.push_back(i - 1);
	}
	vector<vector<bool> > mat = getMat(s);
	for (int i = 1; i < 1 + len; ++i) 
	{
		for (int j = 0; j < i; ++j)
	    {
			if (mat[j][i - 1]) 
			{ 
				cut[i] = min(cut[i], 1 + cut[j]);
			}
		}
	}
	return cut[len];
}
vector<vector<bool> > getMat(string s) 
{
	int len = s.size();
	vector<vector<bool> > mat = vector<vector<bool> >(len, vector<bool>(len,false));
	for (int i = len - 1; i >= 0; --i) {
		for (int j = i; j < len; ++j) {
			if (j == i) 
			{
				// 单字符为回文字符串
				mat[i][j] = true;
			}
			else if (j == i + 1) 
			{
				// 相邻字符如果相同,则为回文字符串
				mat[i][j] = (s[i] == s[j]);
			}
			else 
			{
				// F(i,j) = {s[i]==s[j] && F(i+1,j-1)
				// j > i+1
				mat[i][j] = ((s[i] == s[j]) && mat[i + 1][j - 1]);
			}
		}
	}
	return mat;
}
最长连续子序列问题

例如str1为abcdaf,str2为acbcf,此时他们的最长连续子序列就是abcf
这里使用动态规划的方式来做,首先要确定初始状态
建立一个二维数组,数组的行数是str2的长度,列数是str1的长度,此时数组dp的初始状态就是第0行和第0列都是0,此时进行推导
建立两个循环,比较两个字符串的字符,一旦字符相等,此时这个位置的状态就应该是上一行上一列的状态+1,如果不相等,此时该位置的状态就是上一行和上一列的状态两者比较大的那一个,那么此时状态递推方程就是:
dp[i][j]为:
if(s1[j-1]==s2[i-1]) dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
实现代码:

int main()
{
	string s1, s2;
	cin >> s1 >> s2;
	int m = s1.size();
	int n = s2.size();
	vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
	//先将第0行和第0列都初始化为0
	for (int i = 0; i < n + 1; ++i)
		dp[i][0] = 0;
	for (int i = 0; i < m + 1; ++i)
		dp[0][i] = 0;
	//然后进行实现
	for (int i = 1; i < n + 1; ++i)
	{
		for (int j = 1; j < m + 1; ++j)
		{
			if (s1[j - 1] == s2[i - 1])
				dp[i][j] = dp[i - 1][j - 1] + 1;
			else
				dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
		}
	}
	for (int i = 1; i < n + 1; ++i)
	{
		for (int j = 1; j < m + 1; ++j)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	//输出长度
	cout << dp[n][m] << endl;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 算法个人学习笔记pdf是一种以电子文档形式呈现的算法学习笔记资源。在这个pdf中,个人学习者可以记录和整理自己在学习算法过程中的思考、心得和解题方法。在这个学习笔记pdf中,个人学习者可以自由地添加和编辑自己的学习内容,包括算法的原理、算法实现的代码以及相应的思路和分析。通过这种方式,个人学习者可以更系统地学习和理解算法,并且能够随时查看自己的学习进展和学习果。 通过编写和整理算法个人学习笔记pdf,个人学习者可以更好地理解和应用学习到的算法知识。这种记录和整理的过程可以帮助个人学习者更深入地思考问题和解决问题的方法,同时也可以帮助个人学习者更好地复习和回顾已学习算法知识。 对于其他学习者来说,算法个人学习笔记pdf也是一个宝贵的学习资源。其他学习者可以通过查阅个人学习者的学习笔记pdf,借鉴和学习其中的思路和方法。这样可以帮助其他学习者更好地理解和应用算法知识,同时也可以促进知识的分享和交流。 总的来说,算法个人学习笔记pdf是一个为个人学习者提供记录和整理学习过程的工具,同时也是一个为其他学习者提供学习资源和参考的媒介。通过编写和整理算法个人学习笔记pdf,个人学习者可以更好地学习和理解算法知识,同时也可以促进算法知识的分享和交流。 ### 回答2: 算法个人学习笔记pdf是一份记录个人学习算法的文档,具有以下特点和优势。 首先,这份学习笔记是以PDF格式保存的,这意味着可以在任何设备上方便地查看和阅读,无需依赖特定的平台或软件。无论是在电脑、平板还是手机上,都可以轻松地浏览和学习。 其次,这份学习笔记是个人整理的,因此具有个性化的特点。不同的人在学习算法时可能会关注和理解的重点有所不同,通过个人学习笔记,可以反映出个人对算法知识的理解和思考。这样的学习笔记对于个人的学习和复习过程非常有帮助。 此外,这份学习笔记应当具有清晰的结构和逻辑。算法知识通常是有层次结构的,基本的知识点和概念通常是必须掌握的基础,而进阶的知识则需要在掌握基础知识的基础上构建。学习笔记应当按照这个结构和逻辑进行组织,便于学习者理解和掌握。 最后,这份学习笔记应当具有实例和练习题。算法知识的学习不能仅仅停留在理论层面,还需要通过实际的例子和练习题进行实践和巩固。学习笔记应当包含这些实例和练习题,并给出相应的解析和答案,方便学习者进行练习和巩固。 总而言之,算法个人学习笔记pdf是一份方便、个性化、结构清晰、包含实例和练习题的文档,对于学习者来说非常有价值。 ### 回答3: 算法学习笔记PDF是一份用于记录个人学习算法的文档。通过编写学习笔记,我可以对算法的理论和实践有更深入的理解和掌握。 首先,在学习算法的过程中,理论与实践结合是非常重要的。在学习笔记中,我可以记录下算法的原理和相关的数学推导,以及对应的代码实现和应用场景。通过这样的记录方式,我可以更好地理解算法的本质和使用方式。 其次,学习笔记可以帮助我回顾和巩固所学的知识。通过整理和总结学习笔记,我可以梳理出算法的基础知识和重要思想,并将其记忆固定下来。同时,学习笔记也可以作为复习的资料,提供方便快捷的回顾方式。 此外,学习笔记还可以促进自我思考和学习方法的改进。在编写笔记的过程中,我可以思考和提出自己的问题,并通过查阅相关资料和与他人讨论,来找到问题的答案和解决方案。这样的思考过程可以帮助我提高问题解决的能力和学习效果。 最后,学习笔记可以与他人分享和交流。通过分享学习笔记,我可以与其他学习者进行交流和讨论,互相学习和提高。同时,学习笔记也可以作为自己学习长的见证,激励自己坚持学习和进步。 总之,算法个人学习笔记PDF是一份记录、回顾、思考和分享的文档,对于个人的算法学习具有重要的意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值