刷题笔记笔记三

Posted 小峰同学&&&

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了刷题笔记笔记三相关的知识,希望对你有一定的参考价值。

  1. 凑算式(蓝桥真题)

题目:

注意:需要通分,有些时候除不尽需要通分。

源码:

方法一:递归回溯全排列


int ret = 0;
#define MAX 9
//多少个全排列
int a[MAX];//排列数组
bool flag[MAX];//标记数组

int n = MAX; 
//全排列n个数

void Init()
//初始化标记为
    for (int i = 0; i < MAX; i++)
    
        flag[i] = true;
    


bool check()

    int x = a[3] * 100 + a[4] * 10 + a[5];
    int y = a[6] * 100 + a[7] * 10 + a[8];

    if (((a[1] * y + a[2] * x) % (a[2] * y)  == 0) && a[0] + (a[1] * y + a[2] * x) / (a[2] * y) == 10)
        return true;
    
    return false;


void dfs(int storey)

    if (storey == MAX)//如果深度达到MAX就返回
        //检查
        if (check())
            ret++;
        return;
    
    for (int i = 0; i < MAX; i++)
        if (flag[i] == true)//判断此位置是否被使用       
            flag[i] = false;//此数字被使用。
            a[storey] = i+1;//给该层赋值
            dfs( storey+ 1);//执行下一层
            flag[i] = true;//回溯//让此数字没有被使用。
        
    

int  main()
    Init();
    dfs(0);//递归深度从0开始
    cout << ret;
    return 0;
 

方法二:next_permutation全排列

#include<iostream>
#include<algorithm>
using namespace std;

#define SIZE 9
int a[SIZE] =  1,2,3,4,5,6,7,8,9;
int ret = 0;
bool check()

    int x = a[3] * 100 + a[4] * 10 + a[5];
    int y = a[6] * 100 + a[7] * 10 + a[8];
    //(a[1] * y + a[2] * x) % (y * a[2]) == 0 && a[0] + (a[1] * y + a[2] * x) / (y * a[2]) == 10
    if (((a[1] * y + a[2] * x) % ( y*a[2] )  == 0) && a[0] + (a[1] * y + a[2] * x) / (y*a[2]) == 10)
        return true;
    
    return false;

void test1()
    do 
        if (check() == true)
            ret++;
        
     while (next_permutation(a, a + 9));

int main()

    test1();
    cout << ret;
    return 0;
知识点:
1.next_permutation生成全排列得使用
2.理解递归回溯生成全排列

  1. 三羊献瑞(蓝桥真题)

题目描述:

    祥 瑞 生 辉            a b c d 
+   三 羊 献 瑞            e f g b
--------------------------------------
  三 羊 生 瑞 气         e f c b h
还是可以看成全排列的问题。

代码实现:

#include<iostream>
#include<algorithm>
using namespace std;

int a[10] =  0,1,2,3,4,5,6,7,8,9 ;

bool check()

    int add1 = a[0] * 1000 + a[1] * 100 + a[2] * 10 + a[3];
    int add2 = a[4] * 1000 + a[5] * 100 + a[6] * 10 + a[1];
    int sum = a[4] * 10000 + a[5] * 1000 + a[2] * 100 + a[1] * 10 + a[7];
    if (add1 + add2 == sum  && a[0] != 0 && a[4]!= 0)
    
        cout << add2 << endl;
        return true;
    
    return false;

int main()

    do 
        check();
     while (next_permutation(a, a + 10));
    return 0;
知识点:
next_permutation,全排列。

  1. 方格分割(蓝桥真题)

题目描述:

6x6的方格,沿着格子的边线剪开成两部分。要求这两部分的形状完全相同。

如图:就是可行的分割法。

试计算:包括这3种分法在内,一共有多少种不同的分割方法。注意:旋转对称的属于同一种分割法。

请提交该整数,不要填写任何多余的内容或说明文字。

说明:

如果把样例图案剪开,发现有且只有两个点在边界上,且一定经过 (3,3)点。
以(3,3)为起点进行深搜,深搜到一个边界上的点,那么他的中心对称点相当于也搜过了。
如果发现搜到了边界,那么它的中心对称点也到了边界 沿着已经搜过的点剪开,那么剪开的两个图形为中心对称图形。(要注意最终的结果要除以4)
例如 我们从(3,3)点出发一直向右到边界 , 或一直向左,或一直向上,或一直向下剪出来的图形是同一个。

代码实现

#include<iostream>
#include<algorithm>
using namespace std;
bool vis[7][7];//标记数组
void init()

    //初始化标记数组
    for (int i = 0; i < 7; i++)
    
        for (int j = 0; j < 7; j++)
        
            vis[i][j] = true;
        
    


int ret = 0;

void dfs(int x, int y)

    //走到边线
    if (x == 0 || x == 6 || y == 0 || y == 6)
    
        ret++;
        return;
    
    for (int i = 0; i < 4; ++i)
    
        //四个方向
        //(x+1,y)(x-1,y)(x,y+1)(x,y-1)
        int nx;
        int ny;
        if (i == 0)
             nx = x + 1;
             ny = y;
        
        if (i == 1)
             nx = x - 1;
             ny = y;
        
        if (i == 2)
             nx = x;
             ny = y + 1;

        
        if (i == 3)
             nx = x;
             ny = y - 1;
        

        if (vis[nx][ny])
            vis[nx][ny] = false; vis[6 - nx][6 - ny] = false;
            dfs(nx, ny);
            vis[nx][ny] = true; vis[6 - nx][6 - ny] = true;
        
    

int main()

    init();
    vis[3][3] = false;
    dfs(3, 3);
    cout << ret/4 << endl;//509
    return 0;
知识点:
dfs的深度优先遍历。
  1. 方格填数(蓝桥真题)

显然全排列也能搞定。
对每一种排列数进行判断。

代码实现:

#include<iostream>
#include<algorithm>
using namespace std;
int a[10] =  0,1,2,3,4,5,6,7,8,9 ;
int ret = 0;
int dis(int x, int y)

    if (a[x] - a[y] == 1 || a[x] - a[y] == -1)
    
        return 1;
    
    return  0;

bool check()

    if (dis(0, 1) ==1||
        dis(0, 3) ==1||
        dis(0, 4) ==1||
        dis(0, 5) ==1||

        dis(1, 4) ==1||
        dis(1, 5) ==1||
        dis(1, 6) ==1||
        dis(1, 2) ==1||

        dis(2, 5) ==1||
        dis(2, 6) ==1||

        dis(3, 4) ==1||
        dis(3, 7) ==1||
        dis(3, 8) ==1||

        dis(4, 5) ==1||
        dis(4, 7) ==1||
        dis(4, 8) ==1||
        dis(4, 9) ==1||

        dis(5, 6) ==1||
        dis(5, 8) ==1||
        dis(5, 9) == 1 ||
        dis(6, 9) ==1||

        dis(7, 8) ==1||
        dis(9, 8)==1)
    
        return false;
    
    return true;

int main()

    do 
        if (check())
        
            ret++;
        
     while (next_permutation(a, a + 10));
    cout << ret << endl;//1580
    return 0;
以上是最简单的一种方式,也可以深度递归去一个一个填写,这样代码会不好理解
知识点:
只要还是用全排列,全部填入,然后判断每一个排列即可。
  1. 加法变乘法(蓝桥真题)

其实这个题就说遍历,遍历两个*的位置,
加号变为乘号,前后sum的变化就是加上两者相乘的情况,然后再进去两者本身就可以了。
#include<iostream>
using namespace std;

int main()

    for (int i = 1; i < 50; i++)
    
        for (int j = i + 1 ; j < 50; j++)
        
            int sum = 1225 - i - i - 1 + i * (i + 1) - j - j - 1 + j * (j + 1);
            if (sum == 2015)
            
                cout << i <<"  " << j << endl;
            
        
    
    return 0;
很简单但是稍微带一点点小技巧。
  1. 最大公共子串(蓝桥真题)

题目描述:

给定两个字符串,求出最大公共子串的长度:

分析:

这里要用到一个二位数组。且这个二维数组比两个串行列大一。

代码实现:

#include<iostream>
#include<string>
using namespace std;



int fun(const char* str1, const char* str2)

    int max = 0;//记录最大值
    int len1 = strlen(str1);
    int len2 = strlen(str2);
    int a[256][256];
    memset(a, 0, sizeof(int) * 256 * 256);//首先全部初始化位0;
    for (int i = 0; i < len1; i++)
    
        for (int j = 0; j < len2; j++)
        
            if (str1[i] == str2[j])
            
                a[i + 1][j + 1] = a[i][j] + 1;
                if (a[i + 1][j + 1] > max)
                
                    max = a[i + 1][j + 1];
                
            
        
    
    return max;


int main()

    cout << fun("zhangzxnsk", "axsk") << endl;

    return 0;
  1. 最大黑区域的问题(DFS)

题目描述:

    0,1,1,0,0,1,
    1,1,0,1,0,1,
    0,1,0,0,1,0,
    0,0,0,1,1,1,
    1,0,1,1,1,0,
//假设1是黑块,0是白块,求出黑色的块最大联通块的面积。
//每个块面积是1,
//只有上下左右相邻的才算是联通。斜着链接不是联通。

源码:


//坐标移动的四个方向
int ll[4][2] = 
    1,0,
    -1,0,
    0,1,
    0,-1
;

//原始的二维数组。
int arr[5][6] = 
    0,1,1,0,0,1,
    1,1,0,1,0,1,
    0,1,0,0,1,0,
    0,0,0,1,1,1,
    1,0,1,1,1,0
;

int _max = 0;//当前最大面积
int s = 0;//面积

void dfs(int x, int y)

    s += 1;//面积+1
    arr[x][y] = 0;//记录这里统计过。

    for (int i = 0; i < 4; i++)
    
        int nx = x + ll[i][0];
        int ny = y + ll[i][1];

        if (ny>=0 && ny>=0 && nx <= 4&& ny <= 5 && arr[nx][ny] == 1) dfs(nx, ny);
    


int main()

    for (int i = 0; i < 5; i++)
    
        for (int j = 0; j < 6; j++)
        
            //先找到一个黑区域
            if (arr[i][j] == 1)
            
                //从i,j 开始dfs。
                s = 0;//注意每次dfs时候都要先把面积变为0

                dfs(i, j);

                if (s >_max)
                
                    _max = s;
                

            

        
    
    cout << _max << endl;

    return 0;
  1. 剪邮票(蓝桥真题)

#include<iostream>
#include<algorithm>
using namespace std;
int a[12] =  0,0,0,0,0,0,0,1,1,1,1,1 ;

int  ll[4][2] = 
        1,0,
        -1,0,
        0,1,
        0,-1
;
int flag[3][4] =  0 ;
int m = 0;//记录每一次查找的最大联通数

void dfs(int x, int y)

    flag[x][y] = 0;//表示已经统计过
    m++;
    if (m == 5) return;
    for (int i = 0; i < 4; i++)
    
        int nx = x + ll[i][0];
        int ny = y + ll[i][1];

        if (nx >= 0 && ny >= 0 && nx < 3 && ny < 4 && flag[nx][ny] == 1) 
            dfs(nx, ny);
            //此题不需要回溯
        
    


bool check()

    //在数组中填写标记位。
    int size = 0;
    for (int i = 0; i < 3; i++)
    
        for (int j = 0; j < 4; j++)
        
            flag[i][j] = a[size++];
        
    
    for (int i = 0; i < 3; i++)
    
        for (int j = 0; j < 4; j++)
        
            if (flag[i][j] == 1)//找到其中一个标记位置
            
                m = 0;
                dfs(i, j);
                if (m == 5)
                
                    return true;
                
                
            
        
    
    return false;


int ret = 0;
int main()

    do 
        if (check())
        
            ret++;
        
     while (next_permutation(a, a + 12));
    cout << ret << endl;
    return 0;
  1. 日期问题(蓝桥真题)

题目描述:

讲解:

题目不难但是很考验细心程度,需要控制各种各样的不成立问题,
例如:考虑二月的问题,闰年,大小月份的问题。最后还要去重和排序。

实现代码:


#include<iostream>
#include<string>
#include<set>
using namespace std;
int day[13] =  0,31,28,31,30,31,30,31,31,30,31,30,31 ;

bool isleap(int year)

    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);

string check(int a, int b, int c)//a是年,b是月,c是日

    //处理年
    if (a >= 60 && a < 99)  a += 1900; 
    else if (a >= 0 && a < 59)  a += 2000; 
    else  return ""; 
    //处理月
    if (b < 0 || b>12)return "";
    //处理日
    if (isleap(a))  day[2] = 28; 
    if (day[b] < c)  return "";

    string _year = to_string(a);
    string _monch = to_string(b);
    if (b < 10)  _monch.insert(0, 1, '0'); 
    string _day = to_string(c);
    if (c < 10)  _day.insert(0, 1, '0'); 

    string ret = _year +"-"+ _monch + "-" + _day;
    return ret;


int main()

    string strin;
    cin >> strin;
    //按顺序记录三个数字。
    int a = (strin[0] - '0') * 10 + (strin[1] - '0');
    int b = (strin[3] - '0') * 10 + (strin[4] - '0');
    int c = (strin[6] - '0') * 10 + (strin[7] - '0');
    string str1 = check(a, b, c);
    string str2 = check(c, a, b);
    string str3 = check(c, b, a);

    set<string> ret;
    if (str1 != "") ret.insert(str1);
    if (str2 != "") ret.insert(str2);
    if (str3 != "") ret.insert(str3);

    for (auto e : ret)
    
        cout << e << endl;
    
    return 0;
  1. 买不到的数目(蓝桥真题)

题目描述:

分析:

1.这是一个不定方程的问题。
2.“a*x+b*y = c“有解问题。
3.根据数学原理:
a和b互质,一定存在最大凑不出的数是:“a*b-a-b”。
a和b不互质,即gcd(a,b)>1时,任意的an+bm-1都无法实现,有无限个凑不出的数。

实现代码:

#include<iostream>
using namespace std;
int main()

    int n1, n2;
    while (cin >> n1 >> n2)
    
        cout << n1 * n2 - n1 - n2 << endl;
    
    return 0;
  1. 包子凑数(蓝桥真题)

题目描述:

分析:

1.这是一个不定方程的问题。
2.“a1*x1+a2*x2 ---- an*xn = c“ 有解问题。
3.根据数学原理:
a1,a2,a3---an互质,一定存在最大凑不出的数是:“a*b-a-b”。
a1,a2,a3---an不互质,即gcd(a,b)>1时,任意的an+bm-1都无法实现,有无限个凑不出的数。
4.首先根据n<100 & ai<100,可以得到无法组成的最大数就是10000。也可以根据 a,b无法组成的最大数为a*b-a-b确定是 9800。
5.先求出一组数的最小公倍数,当cgcd!=1时,答案就为INF,因为a*n+b*m-1总是无法组成
6.定义一个maxnumm == 10000长度的数组。
7.遍历1~maxnum,动态规划标记所有能访问的点,未标记的个数就是ans;

实现代码:


#include<iostream>
using  namespace std;

int n = 0;//种类数
int arr[100];//存放包子数
bool flag[10000];
//下标对应的数字,可凑出来标记为true,凑不出来标记为false

//互质是:公约数只有1的两个整数是互质
//最大公约数
int gcd(int a, int b)

    if (b == 0)return a;
    else return gcd(b, a % b);


int g = 0;
int ret = 0;
int main()

    flag[0] = true;//0肯定是能凑出来的。
    for (int i = 1; i < 10000; i++)
    
        flag[i] = false;
    

    cin >> n;
    for (int i = 0; i < n; i++)
    
        cin >> arr[i];//从小到大输入
    
    //数据输入完成

    for (int i = 0; i < n ; i++)
    
        if (i == 0)  g = arr[i]; 
        else  g = gcd(arr[i], g); 

        for (int j = 0; j < 9900; j++)
        
            if (flag[j] == true)  flag[j + arr[i]] = true; 
        
    

    


    if (g != 1) 
        cout << "INF" << endl;
        return 0;
    
    for (int j = 0; j < 9900; j++)
    
        if (!flag[j])  ret++; cout << j << endl;
        
    
    cout << ret << endl;

    return 0;

Leetcode刷题笔记——动态规划

动态规划的⼀般流程就是三步:暴⼒的递归解法 -> 带备忘录的递归解法 -> 迭代的动态规划解法。

就思考流程来说,就分为⼀下⼏步:找到状态和选择 -> 明确 dp 数组/函数的定义 -> 寻找状态之间的关系。

  1. 明确dp[i][j]表示的是什么意思
  2. 找到状态转移方程
  3. 确定初始边界

注意:dp是自下往上的方法,故明确base case或者说边界条件后,根据动态转移方程dp[0] => dp[1] => dp[2] => ... => dp[n]

509. Fibonacci Number

The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,

F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2), for n > 1.
Given n, calculate F(n).

Example 1:

Input: n = 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.
Example 2:

Input: n = 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.
Example 3:

Input: n = 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.
 

Constraints:

0 <= n <= 30

我的解法:暴力递归

class Solution 
public:
    int fib(int n) 
        if (n<=1) return n;
        else return fib(n-1) + fib(n-2);
    
; 

复杂度:递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。

子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。
解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。
所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。

观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 f(18) 被计算了两次,而且你可以看到,以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 f(18) 这一个节点被重复计算,所以这个算法及其低效。

动态规划问题的第一个性质:重叠子问题

第二步,解决方案:带备忘录的递归解法
即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。

一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。

int fib(int N) 
    if (N < 1) return 0;
    // 备忘录全初始化为 0
    vector<int> memo(N + 1, 0);
    return helper(memo, N);

int helper(vector<int>& memo, int n) 
    if (n == 1 || n == 2) return 1;
    if (memo[n] != 0) return memo[n];
    // 未被计算过
    memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
    return memo[n];


现在,画出递归树,你就知道「备忘录」到底做了什么。

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

递归算法的时间复杂度怎么算?子问题个数乘以解决一个子问题需要的时间。

子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 f(1), f(2), f(3) … f(20),数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。

解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。

所以,本算法的时间复杂度是 O(n)。比起暴力算法,是降维打击。

至此,带备忘录的递归解法的效率已经和动态规划一样了。实际上,这种解法和动态规划的思想已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。

啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 触底,然后逐层返回答案,这就叫「自顶向下」。

啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算

第三步:动态规划,重点!!
有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,就叫做 DP table 吧,在这张表上完成「自底向上」的推算岂不美哉!

int fib(int N) 
    vector<int> dp(N + 1, 0);
    dp[1] = dp[2] = 1;
    for (int i = 3; i <= N; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[N];



画个图就很好理解了,而且你发现这个 DP table 特别像之前那个「剪枝」后的结果,只是反过来算而已。实际上,带备忘录的递归解法中的「备忘录」,最终完成后就是这个 DP table,所以说这两种解法其实是差不多的,大部分情况下,效率也基本相同。

这里,引出 「动态转移方程」 这个名词,实际上就是描述问题结构的数学形式:
f ( n ) = 1 , n = 1 , 2 f ( n − 1 ) + f ( n − 2 ) , n > 2 f(n) = \\begincases 1, n = 1, 2 \\\\ f(n - 1) + f(n - 2), n > 2 \\endcases f(n)=1,n=1,2f(n1)+f(n2),n>2
为啥叫「状态转移方程」?为了听起来高端。你把 f(n) 想做一个状态 n,这个状态 n 是由状态 n - 1 和状态 n - 2 相加转移而来,这就叫状态转移,仅此而已。

你会发现,上面的几种解法中的所有操作,例如 return f(n - 1) + f(n - 2),dp[i] = dp[i - 1] + dp[i - 2],以及对备忘录或 DP table 的初始化操作,都是围绕这个方程式的不同表现形式。可见列出「状态转移方程」的重要性,它是解决问题的核心。很容易发现,其实状态转移方程直接代表着暴力解法

千万不要看不起暴力解,动态规划问题最困难的就是第一步,写出状态转移方程,即这个暴力解。优化方法无非是用备忘录或者 DP table,再无奥妙可言。

空间优化:见下

动态规划,空间复杂度简化版
斐波那契数的边界条件是 F ( 0 ) = 0 F(0)=0 F(0)=0 F ( 1 ) = 1 F(1)=1 F(1)=1。当 n > 1 n>1 n>1 时,每一项的和都等于前两项的和,因此有如下递推关系:
F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n)=F(n-1)+F(n-2) F(n)=F(n1)+F(n2)
由于斐波那契数存在递推关系,因此可以使用动态规划求解。动态规划的状态转移方程即为上述递推关系,边界条件为 F ( 0 ) F(0) F(0) F ( 1 ) F(1) F(1)

根据状态转移方程和边界条件,可以得到时间复杂度和空间复杂度都是 O ( n ) O(n) O(n) 的实现。由于 F ( n ) F(n) F(n) 只和 F ( n − 1 ) F(n-1) F(n1) F ( n − 2 ) F(n-2) F(n2) 有关,因此可以使用「滚动数组思想」把空间复杂度优化成 O ( 1 ) O(1) O(1)

pqr=p+q
001
pqr=p+q
011
pqr=p+q
112

…以此类推

class Solution 
public:
    int fib(int n) 
        if (n < 2) 
            return n;
        
        int dp0 = 0, dp1 = 0, dp2 = 1;
        for (int i = 2; i <= n; ++i) 
            dp0 = dp1; 
            dp1 = dp2; 
            dp2 = dp0+dp1;
        
        return dp2;
    
;

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

1137. N-th Tribonacci Number 第 N 个泰波那契数

The Tribonacci sequence Tn is defined as follows:

T0 = 0, T1 = 1, T2 = 1, and Tn+3 = Tn + Tn+1 + Tn+2 for n >= 0.

Given n, return the value of Tn.

Example 1:
Input: n = 4
Output: 4
Explanation:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4

Example 2:
Input: n = 25
Output: 1389537
 
Constraints:
0 <= n <= 37
The answer is guaranteed to fit within a 32-bit integer, ie. answer <= 2^31 - 1.

我的方法:同斐波拉契数列解法,动规,滚动数组

class Solution 
public:
    int tribonacci(int n) 
        if (n<2) return n;
        if (n==2) return 1;
        int dp0 = 0, dp1 = 0, dp2 = 1, dp3 = 1;
        for (int i=3; i<=n; i++)
        
            dp0 = dp1;
            dp1 = dp2;
            dp2 = dp3;
            dp4 = dp1+dp2+dp3;
        
        return dp4;
    
;

70. Climbing Stairs 实际上等价于斐波那契数列

You are climbing a staircase. It takes n steps to reach the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Example 1:
Input: n = 2
Output: 2
Explanation: There are two ways to climb to the top.
1. 1 step + 1 step
2. 2 steps
3. 
Example 2:
Input: n = 3
Output: 3
Explanation: There are three ways to climb to the top.
4. 1 step + 1 step + 1 step
5. 1 step + 2 steps
6. 2 steps + 1 step
 
Constraints:
1 <= n <= 45

我的解法:备忘录,自下而上

class Solution 
public:
    int helper(vector<int>& memo, int n) 
        if (n == 1 || n == 2) return n;
        if (memo[n] != 0) return memo[n];
        // 未被计算过
        memo[n] = helper(memo, n - 1) + helper(memo, n - 2);
        return memo[n];
    

    int climbStairs(int n) 
        vector<int> memo(n+1, 0);
        return helper(memo, n);
    
;

凑零钱问题

动态规划的另一个重要特性「最优子结构」,怎么没有涉及?下面会涉及。斐波那契数列的例子严格来说不算动态规划,以上旨在演示算法设计螺旋上升的过程。当问题中要求求一个最优解或在代码中看到循环和 max、min 等函数时,十有八九,需要动态规划大显身手。下面,看第二个例子,凑零钱问题,有了上面的详细铺垫,这个问题会很快解决。

  • 题目:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,再给一个总金额 n,
    问你最少需要几枚硬币凑出这个金额,如果不可能凑出,则回答 -1 。
    比如说,k = 3,面值分别为 1,2,5,总金额 n = 11,那么最少需要 3 枚硬币,即 11 = 5 + 5 + 1

记住,要符合「最优子结构」,子问题间必须互相独立。为什么说它符合最优⼦结构呢?⽐如你想求 amount =11 时的最少硬币数(原问题),如果你知道凑出 amount = 10 的最少硬币数(⼦问题),你只需要把⼦问题的答案加⼀(再选⼀枚⾯值为 1 的硬币)就是原问题的答案,因为硬币的数量是没有限制的,⼦问题之间没有相互制,是互相独⽴的。

下面走流程。
一、暴力解法
首先是最困难的一步,写出状态转移方程,这个问题比较好写:

如何列出正确的状态转移⽅程?

  • 先确定「状态」,也就是原问题和⼦问题中变化的变量。由于硬币数量⽆限,所以唯⼀的状态就是⽬标⾦额 amount
  • 然后确定 dp 函数的定义:当前的⽬标⾦额是 n ,⾄少需要 dp(n) 个硬币凑出该⾦额。
  • 然后确定「选择」并择优,也就是对于每个状态,可以做出什么选择改变当前状态。具体到这个问题,⽆论当的⽬标⾦额是多少,选择就是从⾯额列表 coins 中选择⼀个硬币,然后⽬标⾦额就会减少:
  • 最后明确 base case,显然⽬标⾦额为 0 时,所需硬币数量为 0;当⽬标⾦额⼩于 0 时,⽆解,返回 -1:

d p ( n ) = 0 , n = 0 − 1 , n < 0 m i n d p ( n − c o i n ) + 1 ∣ c o i n ∈ c o i n s , n > 0 dp(n) = \\begincases 0, n = 0 \\\\ -1, n < 0 \\\\ min\\dp(n - coin) + 1 | coin \\in coins\\, n > 0 \\endcases dp(n)=0,n=01,n<0mindp(ncoin)+1coincoins,n>0

其实,这个方程就用到了「最优子结构」性质:原问题的解由子问题的最优解构成。即 f(11) 由 f(10), f(9), f(6) 的最优解转移而来。

int coinChange(vector<int>& coins, int amount) 
	// base case
    if (amount == 0) return 0;
    if (amount <0) return -1;
    // 求最小值,初始化为正无穷
    int ans = INT_MAX;
    for (int coin : coins) 
        int subProb = coinChange(coins, amount - coin); // 1、当amount-coin=1时,subprob=1
        // 子问题无解
        if (subProb == -1) continue;
        ans = min(ans, subProb + 1); // 2、ans就是一个计步用的变量
    
    return ans == INT_MAX ? -1 : ans;


⾄此,这个问题其实就解决了,只不过需要消除⼀下重叠⼦问题,⽐如 amount = 11, coins = 1,2,5 时画出递归树:

时间复杂度分析:子问题总数 x 每个子问题的时间
子问题总数为递归树节点个数,这个比较难看出来,是 O ( n k ) O(n^k) O(nk),总之是指数级别的。每个子问题中含有一个 for 循环,复杂度为 O ( k ) O(k) O(k)。所以总时间复杂度为 O ( k ∗ n k ) O(k*n^k) O(knk),指数级别。

二、带备忘录的递归

int coinChange(vector<int>& coins, int amount) 
    // 备忘录初始化为 -2
    vector<int> memo(amount + 1, -2);
    return helper(coins, amount, memo);


int helper(vector<int>& coins, int amount, vector<int>& memo) 
   	// base case
    if (amount == 0) return 0;
    if (amount <0) return -1;
    // 查询备忘录
    if (memo[amount] != -2) return memo[amount]; //👀
    int ans = INT_MAX;
    
    for (int coin : coins) 
        int subProb = helper(coins, amount - coin, memo);
        // 子问题无解
        if (subProb == -1) continue;
        ans = min(ans, subProb + 1);
    
    // 记录本轮答案
    memo[amount] = (ans == INT_MAX) ? -1 : ans;
    return memo[amount];


三、动态规划
当然,我们也可以⾃底向上使⽤ dp table 来消除重叠⼦问题, dp 数组的定义和刚才 dp 函数类似,定义也是⼀样的:

以上是关于刷题笔记笔记三的主要内容,如果未能解决你的问题,请参考以下文章

算法笔记8.1 深度优先搜索DFS

数据结构学习笔记——图的遍历(深度优先搜索和广度优先搜索)

2021/6/13 刷题笔记括号生成与回溯法(深度优先遍历)

算法笔记第一期——深度优先搜索

MetInfo操作笔记

LeetCode Java刷题笔记—104. 二叉树的最大深度