代码随想录刷题记录

贪心算法

分发饼干

力扣题目链接(opens new window)

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

用尽量小的饼干喂饱孩子。

摆动序列

力扣题目链接(opens new window)

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int res = 1;
        vector<vector<int>> dp(nums.size(), vector<int>(2, 1));
        // dp[i][0] 代表 nums[i]作为谷底的最长摆动序列长度
        // dp[i][1] 代表 nums[i]作为谷顶的最长摆动序列长度
        for (int i = 0; i < nums.size(); ++i) {
            int sheng = 0, jiang = 0;
            for (int j = i - 1; j >= 0; --j) {
                if (nums[i] > nums[j]) {
                    sheng = max(sheng, dp[j][0]);
                } else if (nums[i] < nums[j]) {
                    jiang = max(jiang, dp[j][1]);
                }
            }
            dp[i][0] += jiang;
            dp[i][1] += sheng;
            res = max({res, dp[i][0], dp[i][1]});
        }
        return res;
    }
};

最大子序和

力扣题目链接(opens new window)

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

将数组作为dp就好,记录当前元素结尾的最大和连续子数组。

跳跃游戏

力扣题目链接(opens new window)

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

记录目前能达到的最右边界,如果当前位置加上跳跃长度大于边界,就更新边界。

跳跃游戏 II

力扣题目链接(opens new window)

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

maxPos记录目前能达到的最右边界,如果当前位置加上跳跃长度大于边界,就更新边界。

end记录当前步数能达到最右边界,当到达end时,步数加一,并且将end更新为maxPos

K次取反后最大化的数组和

力扣题目链接(opens new window)

给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)

以这种方式修改数组后,返回数组可能的最大和。

先把绝对值大的负数 变为负数,若全部负数都变为正数。剩下的k还为奇数,就选一个绝对值最小的数变为负数。

加油站

力扣题目链接(opens new window)

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

class Solution {
public:
    /*
    直接从全局进行贪心选择,情况如下:
    情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
    情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
    情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
    */
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int min = INT_MAX; // 从起点出发,油箱里的油量最小值
        for (int i = 0; i < gas.size(); i++) {
            int rest = gas[i] - cost[i];
            curSum += rest;
            if (curSum < min) {
                min = curSum;
            }
        }
        if (curSum < 0) return -1;  // 情况1
        if (min >= 0) return 0;     // 情况2
                                    // 情况3
        for (int i = gas.size() - 1; i >= 0; i--) {
            int rest = gas[i] - cost[i];
            min += rest;
            if (min >= 0) {
                return i;
            }
        }
        return -1;
    }
};

分发糖果

力扣题目链接(opens new window)

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

class Solution {
public:
    // 两次贪心。第一次从左往右。第二次从右往左
    int candy(vector<int>& ratings) {
        vector<int> candiesVec(ratings.size(), 1);
        for (int i = 1; i < ratings.size(); ++i) {
            if (ratings[i] > ratings[i - 1]) {
                candiesVec[i] = candiesVec[i - 1] + 1;
            }
        }
        for (int i = ratings.size() - 2; i >= 0; --i) {
            if (ratings[i] > ratings[i + 1]) {
                // 这里一定要加判断,要去最大值
                candiesVec[i] = max(candiesVec[i], candiesVec[i + 1] + 1);
            }
        }
        return accumulate(candiesVec.begin(), candiesVec.end(), 0);
    }
};

柠檬水找零

力扣题目链接(opens new window)

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

优先用10去找零。

根据身高重建队列

力扣题目链接(opens new window)

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

// 版本一
class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {	
        // 将身高高的排前面,身高相同的,[1]小的放前面
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        vector<vector<int>> que;
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1];
            que.insert(que.begin() + position, people[i]);
        }
        return que;
    }
};

用最少数量的箭引爆气球

力扣题目链接(opens new window)

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
     		// 以区间结尾排序
            return a[1] < b[1]; 
        });
        int shotpos = points[0][1], shotcnt = 1;
        for (auto& vec : points) {
            if (vec[0] > shotpos) {
                ++shotcnt;
                shotpos = vec[1]; /*以区间结尾为射击位置,尽可能射更多的气球*/
            }
        }
        return shotcnt;
    }
};

无重叠区间

力扣题目链接(opens new window)

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

示例 1:

  • 输入: [ [1,2], [2,3], [3,4], [1,3] ]
  • 输出: 1
  • 解释: 移除 [1,3] 后,剩下的区间没有重叠。

先让右边界小的排除,这样的区间会影响尽可能少的区间。

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), [](const vector<int>& lhs, const vector<int>& rhs) {
            return lhs[1] < rhs[1];
        });
        int cnt = 0, end = INT_MIN;
        for (const auto& interval : intervals) {
            if (interval[0] >= end) {
                end = interval[1];
            } else {
                ++cnt;
            }
        }
        return cnt;
    }
};

划分字母区间

力扣题目链接(opens new window)

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

  • 输入:S = "ababcbacadefegdehijhklij"
  • 输出:[9,7,8] 解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。

记录每个字母最后一次出现的位置,然后双指针start,end。移动i,若map[s[i]]比end大,更新end。当i等于end,表明区间出现,记录长度。

合并区间

力扣题目链接(opens new window)

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

  • 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
  • 输出: [[1,6],[8,10],[15,18]]
  • 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

将区间按照左边界从小到大排序。然后遍历,合并

单调递增的数字

力扣题目链接(opens new window)

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

示例 1:

  • 输入: N = 10
  • 输出: 9
class Solution {
public:
    int monotoneIncreasingDigits(int N) {
        string strNum = to_string(N);
        // flag用来标记赋值9从哪里开始
        // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
        int flag = strNum.size();
        for (int i = strNum.size() - 1; i > 0; i--) {
            // 找到第一个大于其后面的数字,将其减一
            if (strNum[i - 1] > strNum[i] ) {
                flag = i;
                strNum[i - 1]--;
            }
        }
        // 将其后面的数字都置为9
        for (int i = flag; i < strNum.size(); i++) {
            strNum[i] = '9';
        }
        return stoi(strNum);
    }
};

监控二叉树

力扣题目链接(opens new window)

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

动态规划

爬楼梯

斐波那契数

爬楼梯

这两道都是斐波那契数列

使用最小花费爬楼梯

746. 使用最小花费爬楼梯

  • 题目:

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。

  • 支付 15 ,向上爬两个台阶,到达楼梯顶部。
    总花费为 15 。
  • 定义:dp[i]代表爬到第i个台阶所需最小花费,
  • 遍历: dp[i] = min(dp[i - 1] + cost[i - 1] , dp[i - 2] + cost[i-2] ) . return dp[n]

背包类问题

01背包

分割等和子集

力扣题目链接(opens new window)

  • 题目: 转换成01背包,数组的值即是重量,也是价值。背包大小为数组总和的一半,求背包能装的最大价值是否为数组总和一半

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

示例 1:

  • 输入: [1, 5, 11, 5]
  • 输出: true
  • 解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

  • 输入: [1, 2, 3, 5]
  • 输出: false
  • 解释: 数组不能分割成两个元素和相等的子集.
  • 定义:dp[i] [j] 在 (0 ~ i - 1) 件物品, 背包大小为j,能装的最大价值

  • 初始化 dp[n + 1] [halfsum + 1] =

  • 遍历:注意j的遍历顺序,从后往前,否则前面的物品会被取多次,变成完全背包

for (i = 1; i <= n; ++i) {
    for (j = halfsum; j >= 1; ++j) {
        if (j >= nums[i - 1]) {
            dp[j] = max(dp[j], dp[j - nums[i-1]] + nums[i - 1]);
        }
    }
}

最后一块石头的重量II

力扣题目链接(opens new window)

  • 题目:这还是01背包,石头价值和重量,将所有石头重量一半作为背包大小,这样尽可能装多的石头,这样最后:abs(allweight - dp[halfweight] - dp[halfweight])

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

示例:

  • 输入:[2,7,4,1,8,1]
  • 输出:1

解释:

  • 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
  • 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
  • 组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
  • 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

目标和

力扣题目链接(opens new window)

  • 题目:注意转变思路:一半加+,一半加-,也就是 left + right = sum; left - right = target; left = (sum + target)/ 2。则还是变为01背包,在数组中取一些数,其总和等于left。特别注意,题目要求的是组合数(这里理解就算值相同,下标不同就是不同的数)。

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5

解释:

  • -1+1+1+1+1 = 3
  • +1-1+1+1+1 = 3
  • +1+1-1+1+1 = 3
  • +1+1+1-1+1 = 3
  • +1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

  • 数组非空,且长度不会超过 20 。
  • 初始的数组的和不会超过 1000 。
  • 保证返回的最终结果能被 32 位整数存下。
  • 定义:dp[i] [j] 代表 nums[0 ~ i-1]这些数中,组成和为j的组合数一共有dp[i] [j]种方法
  • 初始化:dp[i] [0] = 1; dp[0] [j] = 0; dp[0] [0] = 1;
  • 遍历:
for (i = 1; i <=n; ++i) {
	for (j = left; j >= 1; --j) {
        dp[i][j] += dp[i-1][j - nums[i]];
        // 一维:dp[j] += dp[j - nums[i]];
    } 	
}

return dp[left];

一和零

力扣题目链接(opens new window)

  • 题目:其实还是01背包,不过这个背包是3维的。第1维度还是物品,2和3维度则是0和1的个数

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

  • 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
  • 输出:4
  • 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
  • 定义:dp[a] [b] [c] 代表 在0 ~ a -1 个物品中,最多b个0,c个1,最大子集数量
  • 初始化:dp[k] [m] [n] =
  • 遍历:注意遍历顺序,m和n是从大到小
for (a = 1; a < k; ++a) {
	for (b = m; b >= 1; ++b) {
		for (c = n; c >= 1; ++c) {
			if (strs[i-1]的0个数 <= b && strs[i-1]的1个数 <= c)
			dp[i][b][c] = max(dp[i][b][c], dp[i-1][b - strs[i-1]的0个数][strs[i-1]的1个数] + 1);
		}
	}
}
  • 优化:其实a的那层完全没有必要,可以将最外层去掉,因为都是算完i-1再算i。(由m和n是从大到小 保证,如果不是,则需要添加多一层循环)并且都是先算完小的dp[m] [n]再算大的,所以不用担心覆盖

完全背包

零钱兑换II

力扣题目链接(opens new window)

  • 题目:"每一种面额的硬币有无限个"就代表完全背包,完全背包与01背包的就是内层遍历背包大小的循环换过来。01:从大到小,完全:从小到大。题目求的是组合数

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

  • 输入: amount = 5, coins = [1, 2, 5]
  • 输出: 4

解释: 有四种方式可以凑成总金额:

  • 5=5
  • 5=2+2+1
  • 5=2+1+1+1
  • 5=1+1+1+1+1

注意,你可以假设:

  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数
  • 定义:dp[i] [j] 代表用 0~i-1种硬币,凑成面额为j的组合数
  • 初始化dp[i] [j] = 0; dp[0] [0] = 1;
  • 遍历
for (i = 1; i <= n; ++i) {
	for (j = 1; j <= amount; ++j) {
		if (coins[i-1] <= j) {
			dp[j] += dp[j-coins[i-1]]; 
		}
	}
}

组合总和 Ⅳ

力扣题目链接(opens new window)

  • 题目:完全背包,而且求的是排列数。

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

  • 定义:dp[i] [j] 从 0到i-1种硬币,组成面额j的排列数
  • 初始化:dp[i] [0] = 1, else = 0
  • 遍历

对于完全背包“

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

for (j = 1; j <= target; ++j) {
	for (i = 1; i <= n; ++i) {
		if (j >= coins[i - 1]) {
			dp[i][j] += dp[i-1][j-conis[i-1]];
		}
	}
}

return dp[n][target]

爬楼梯(进阶版)

卡码网:57. 爬楼梯(opens new window)

  • 题目:1-m个台阶代表物品,目前台阶j代表背包大小,”多少种方法“代表有先后顺序,而且是完全背包,则与上面的 组合求和Ⅳ相同。注意先遍历背包,再遍历物品。

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

输入描述:输入共一行,包含两个正整数,分别表示n, m

输出描述:输出一个整数,表示爬到楼顶的方法数。

输入示例:3 2

输出示例:3

提示:

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

  • 1 阶 + 1 阶 + 1 阶段
  • 1 阶 + 2 阶
  • 2 阶 + 1 阶

零钱兑换

力扣题目链接(opens new window)

  • 题目:"每种硬币的数量是无限的"代表完全背包。而且是组合数,从而确定遍历顺序

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

示例 3:

  • 输入:coins = [1], amount = 0
  • 输出:0
  • 定义:dp[i] [j] >= 0 代表可以用 0 到 i-1种硬币凑成面额j用的最少硬币数。为INT_MAX则代表不能。
  • 初始化:dp[i] [0] 全部为0, 其余都为INT_MAX
  • 遍历:求组合,而且完全背包。这道题目要注意:你尽量不用二维矩阵做(下面有做),因为dp[i] [j] 依赖的很复杂他不仅依赖dp[i-1] [j - coin], 而且还依赖最新的 dp[i] [j - coin],所以你每次都需要新复制上一行,那这样不如用一维的,而且注意背包的遍历顺序,从小到大,因为大的依赖小的。建议与 零钱兑换Ⅱ 对比一下。
for (i = 1; i <= n; ++i) {
	for (j = amount; j >= 0; --j) {
		if (j >= coins[i-1]) {
            dp[i][j] = min(dp[i][j], dp[i-1][j-coins[i-1]]+1);
        }		
	}
}

return dp[n][amount] != INT_MAX ? dp[n][amount] : -1;

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (amount == 0) return 0;
        int n = coins.size();
        vector<long long> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = coins[i]; j <= amount; ++j) {
                dp[j] = min(dp[j], dp[j-coins[i]]+1);
            }
        }
        if (dp[amount] == INT_MAX) return -1;
        return dp[amount];
         
        // 二维
        vector<vector<long long>> dp(n + 1, vector<long long>(amount + 1, INT_MAX));
        for (int j = 1; j <= amount; ++j) dp[0][j] = INT_MAX;   // 没有硬币
        for (int i = 0; i <= n; ++i) dp[i][0] = 0;              // 总额为0
        for (int i = 1; i <= n; ++i) {
            dp[i] = dp[i-1];	// 注意这一行
            for (int j = coins[i-1]; j <= amount; ++j) {
            	dp[i][j] = min({dp[i][j], dp[i][j-coins[i-1]]+1});	
            }
        }
        return dp[n][amount] != INT_MAX ? dp[n][amount] : -1;
        
        
    }
};

完全平方数

力扣题目链接(opens new window)

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

平方数作为物品,先遍历物品,再从小到大遍历背包,就是完全背包,因为每个平方数可以使用多次

class Solution {
public:
    int numSquares(int n) {
        // if (sqrt(n) * sqrt(n) == n) return 1;
        // dp[i]代表和为i的完全平方数的最少数量
        vector<int> dp(n + 1);
        dp[0] = 0;
        for (int i = 1; i <= n; ++i) {
            dp[i] = i;
        }
        for (int i = 1, square = i * i; square <= n; ++i, square = i * i) {
            for (int j = square; j <= n; ++j) {
                if (dp[j - square] + 1 < dp[j]) {
                    dp[j] = dp[j - square] + 1;
                }
            }
        }
        return dp[n];
    }
};

打家劫舍

打家劫舍

力扣题目链接(opens new window)

dp[i] = (dp[i - 2] + nums[i], dp[i - 1])

打家劫舍II

力扣题目链接(opens new window)

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。

去除头和去除尾,用打家劫舍1的方法求两次,求最大值就可以

打家劫舍 III

力扣题目链接(opens new window)

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

class Solution {
public:
    int rob(TreeNode* root) {
        if (!root) return 0;
        if (!root->left && !root->right) {
            return root->val;
        }
        if (cache[root]) {
            return cache[root];
        }
        int rootval = root->val;
        if (root->left) rootval += rob(root->left->left) + rob(root->left->right);	// 不偷root->left
        if (root->right) rootval += rob(root->right->left) + rob(root->right->right);	// 不偷root->right
        int nonrootval = rob(root->left) + rob(root->right);	// 不偷root
        int res = max(rootval, nonrootval);
        cache[root] = res;	// 记录当前节点为根,能盗取的最大金额
        return res;
    }

private:
    unordered_map<TreeNode*, int> cache;
    int res = 0;
};

买股票问题

买卖股票的最佳时机

力扣题目链接(opens new window)

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

  • 示例 1:
  • 输入:[7,1,5,3,6,4]
  • 输出:5
    解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

初始时,将第一天作为买入的时候,后面遍历数组,当大于买入价格时,记录差值。当小于买入价格时,更新买入时间。

买卖股票的最佳时机II

力扣题目链接(opens new window)

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

  • 示例 1:
  • 输入: [7,1,5,3,6,4]
  • 输出: 7
    解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // dp[i][0] 表示第i天持有股票所得现金。
        // dp[i][1] 表示第i天不持有股票所得最多现金
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        // 持有股票需要和持有股票的状态比较
        // 没买股票需要和没买股票的状态比较
        for (int i = 1; i < len; i++) {
            // dp[i - 1][0]: 继续持有股票,dp[i - 1][1] - prices[i]:第i天买入股票
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); 
            // dp[i - 1][1]:继续不买股票,dp[i - 1][0] + prices[i]:第i天卖出股票
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
        }
        return dp[len - 1][1];
    }
};

买卖股票的最佳时机III

力扣题目链接(opens new window)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

  • 示例 1:
  • 输入:prices = [3,3,5,0,0,3,1,4]
  • 输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3。
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // dp[i][0]: 没有任何操作
        // dp[i][1]: 第一次持有股票
        // dp[i][2]: 第一次不持有股票
        // dp[i][3]: 第二次持有股票
        // dp[i][4]: 第二次不持有股票
        int len = prices.size();
        // 第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
        // 第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
        // 所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
        vector<vector<int>> dp(len, vector<int>(5, 0));
        dp[0][1] = dp[0][3] = -prices[0];
        // dp[0][0] = dp[0][1] = dp[0][3] = 0;
        // 持有股票需要和持有股票的状态比较
        // 没买股票需要和没买股票的状态比较
        for (int i = 1; i < len; i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return max({dp[len - 1][0], dp[len - 1][2], dp[len - 1][4]});
    }
};

买卖股票的最佳时机IV

力扣题目链接(opens new window)

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

  • 示例 1:
  • 输入:k = 2, prices = [2,4,1]
  • 输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2。

和Ⅲ一样

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if (prices.size() == 0) return 0;
        // dp[i][0]: 没有任何操作
        // dp[i][2*k-1]: 第k次持有股票
        // dp[i][2*k]: 第k次不持有股票
        vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
        for (int i = 1; i < 2 * k + 1; i += 2) dp[0][i] = -prices[0];
        for (int i = 1; i < prices.size(); i++) {
            for (int j = 1; j < 2 * k + 1; j += 2) {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
            }
        }
        return dp[prices.size() - 1][2 * k];
    }
};

子序列或连续序列问题

最长递增子序列

力扣题目链接

  • 题目

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

  • dp定义:dp[i] 代表 以nums[i]为结尾的最长递增子序列的长度
  • 初始化:dp数组都为1
  • 遍历:
for (i = 1; i < n; ++i) {
	for (j = 0; j < i; ++j) {
		if (nums[i] > nums[j]) {
			dp[i] = max(dp[i], dp[j] + 1);
		}
		res = max(res, dp[i]);
	}
}

最长连续递增序列

力扣题目链接

  • 题目:

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

  • dp定义:dp[i] 代表 以nums[i]为结尾的最长连续递增序列的长度
  • 初始化:dp数组都为1
  • 遍历:
for (i = 0; i < n - 1; ++i) {
	if (nums[i + 1] > nums[i]) 
		dp[i + 1] = dp[i] + 1;
}

最长重复子数组

力扣题目链接

  • 题目:注意,这里是子数组,不是子序列

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:

  • A: [1,2,3,2,1]
  • B: [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3, 2, 1] 。
  • dp定义:dp[ i ] [ j ] 代表 以 A[i-1]和B[j-1]为结尾的最长的子数组的长度
  • 初始化:dp[0] [j] = dp[i] [0] = 0
  • 遍历
dp [lena+1][lenb+1];
for (i = 1; i <= lena; ++i) {
    for (j = 1; j <= lenb; ++j) {
        if (A[i - 1] == B[j - 1]) {
            dp[i][j] = dp[i - 1][j - 1] + 1;
        } else {
            // /*这里是子序列的写法*/dp[i][j] = max(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]);
            dp[i][j] = 0;
        }
    }
}

最长公共子序列

力扣题目链接(opens new window)

  • 题目:

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

  • 输入:text1 = "abcde", text2 = "ace"
  • 输出:3
  • 解释:最长公共子序列是 "ace",它的长度为 3。
  • dp定义:dp[i] [j] 代表text1[0 ~ i-1] 和 text2[0 ~ j-1]两个子数组最长公共子序列的长度
  • 初始化:dp[0] [j] = dp[i] [0] = 0
  • 遍历
dp[n][m]; n:text1.size m:text2.size
for (i = 1; i <= n; ++i) {
    for (j = 1; j <= m; ++j) {
        if (text1[i-1] == text2[j-1]) {
            dp[i][j] = dp[i-1][j-1] + 1;	// 二维
            // dp[j] = dp[j-1] + 1;	// 一维
        } else {
            dp[i][j] = max({dp[i - 1][j], dp[i][j-1]}); // 二维
            // dp[j] = max({dp[j], dp[j-1]}); // 一维
        }
    }
}

不相交的线

力扣题目链接

  • 题目:这道题主要是理解,找规律,注意不能相交这个点。连线要在两个相同的数字,不能相交:要求按照相对顺序来。那不就是找按相对顺序的相同数字的对数,那不就是求最长子序列。

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。

现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。

以这种方法绘制线条,并返回我们可以绘制的最大连线数。

  • dp定义:dp[i] [j] 为 A[0 ~ i-1] 和 B[0 ~ j-1] 最长相同子序列的长度
  • 初始化:dp[0] [j] = dp[i] [0] = 0
  • 遍历
for (i = 1; i <= n; ++i) {
    for (j = 1; j <= m; ++j) {
        if (A[i - 1] == B[i - 1]) {
            dp[i][j] = dp[i - 1][j - 1] + 1;
        } else {
            dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
        }
    }
}
return dp[n][m]

判断子序列

力扣题目链接(opens new window)

  • 题目:其实还是求公共子序列。求s和t的最长子序列,看是不是为s本身
  • 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:

  • 输入:s = "abc", t = "ahbgdc"
  • 输出:true

示例 2:

  • 输入:s = "axc", t = "ahbgdc"
  • 输出:false

提示:

  • 0 <= s.length <= 100
  • 0 <= t.length <= 10^4

两个字符串都只由小写字符组成。

不同的子序列

力扣题目链接(opens new window)

  • 题目:

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

  • dp定义:dp[i] [j] 代表 s[0 ~ i-1] 的子序列中出现 t[0 ~ j-1]的个数
  • 初始化:dp[0] [j] = dp[i] [0] = 0, dp[0] [0] = 1
  • 遍历:
n = s.size();
m = t.size();
for (i = 1; i <= n; ++i) {
    for (j = 1; j <= m; ++j) {
        if (s[i-1] == t[j-1]) {
			dp[i][j] = dp[i-1][j] /*s[i-1]不参与匹配*/ + dp[i-1][j-1]/*s[i-1]参与匹配*/;
        } else {
            dp[i][j] = dp[i-1][j]	/*s[i-1]不参与匹配*/
        }
    }
}

return dp[n][m]

编辑距离

两个字符串的删除操作

力扣题目链接(opens new window)

  • 题目:
  • 给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: "sea", "eat"
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
  • dp定义:dp[i] [j] 代表 word1[0 ~ i-1] 和 word2[0 ~ j-1] 相同需要的最小步数
  • 初始化:dp[0] [j] = j ; dp[i] [0] = i ; dp[0] [0] = 0
  • 遍历:
for (i = 1; i <= n; ++i) {
	for (j = 1; j <= m; ++j) {
        if (word1[i - 1] == word2[j - 1]) {
            dp[i][j] = dp[i-1][j-1];
        } else {
            dp[i][j] = min({dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+2});
        }
    }
}

return dp[n][m]

编辑距离

力扣题目链接(opens new window)

  • 题目:

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
  • 示例 1:
  • 输入:word1 = "horse", word2 = "ros"
  • 输出:3
  • 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
  • 示例 2:
  • 输入:word1 = "intention", word2 = "execution"
  • 输出:5
  • 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
  • dp定义:dp[i] [j] 代表 word1[0 ~ i-1] 和 word2[0 ~ j-1] 相同需要的最小编辑次数步数
  • 初始化:dp[0] [j] = j ; dp[i] [0] = i ; dp[0] [0] = 0
  • 遍历:
for (i = 1; i <= n; ++i) {
	for (j = 1; j <= m; ++j) {
        if (word1[i - 1] == word2[j - 1]) {
            dp[i][j] = dp[i-1][j-1];
        } else {
            dp[i][j] = min(
                {
      dp[i-1][j]+1/*删除或修改word1[i-1]*/, 		
      dp[i][j-1]+1/*删除或修改word2[j-1]*/, 
/*没必要*/ dp[i-1][j-1]+2/*删除word1[i-1]和word2[i-2]*/,
                });
        }
    }
}

return dp[n][m]

回文

回文子串

力扣题目链接(opens new window)

  • 题目:这道题dp不好作,用双指针遍历更简单

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:"abc"
  • 输出:3
  • 解释:三个回文子串: "a", "b", "c"
class Solution {
public:
    int helper(const string& s, int left, int right) {
        int ret = 0;
        while (left >= 0 && right < s.size()) {
            if (s[left] == s[right]) {
                --left;
                ++right;
                ++ret;
            } else {
                break;
            }
        }
        return ret;
    }
    int countSubstrings(string s) {
        int ret = 0;
		for (int i = 0; i < s.size(); ++i) {
            ret += helper(s, i, i + 1);
            ret += helper(s, i, i);
        }
        return ret;
    }
};

最长回文子序列

力扣题目链接(opens new window)

  • 题目: 注意是子序列,不是连续的

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。

示例 2: 输入:"cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。

  • dp定义:dp[i] [j] 代表 以s[i ~ j]的最长回文子序列的最长长度
  • 初始化:dp[i] [i] = 1; else = 0
  • 遍历:这里注意遍历顺序。(因为上一行(i)是依赖下一行(i+1),所以需要先计算下一行)。并且 i > j 时,dp[i] [j] 无意义,所以j是从i+1开始
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
        for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
        for (int i = s.size() - 1; i >= 0; i--) {
            for (int j = i + 1; j < s.size(); j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][s.size() - 1];
    }
};
posted @ 2024-07-12 16:48  DavidJIAN  阅读(38)  评论(0)    收藏  举报