动态规划
动态规划核心问题怎么找DP,如果是矩阵路径类的题很容易想到dp[i][j],但是并非所有题都可以清楚找到DP项。
DP引子:斐波那契额数列
剑指 Offer 10- II. 青蛙跳台阶问题
//该题为斐波那契额数列变种,是dp问题中最经典题型,dp[i] = dp[i-1]+dp[i-2],由于dp只与前两项有关,所以可以优化dp将空间复杂度降低为O(1)
class Solution{
public:
int numWays(int n) {
if(n<=1) return 1;
int left = 1;
int right =1;
int res = 0;
for(int i = 2;i<= n;++i){
res = (left+right)%1000000007;
left = right;
right = res;
}
return res;
}
};
股票问题
122. 买卖股票的最佳时机 II
无限制买卖次数,不过每次买卖需要手续费
//dp[i][0]表示当天的收益(即选择是否在今天进行卖出后的收益)
//dp[i][1]表示当天买入股票后的剩余的资金
//dp[i][0] = dp[i-1][0] or dp[i-1][1]+prices[i] 。dp[i-1][0]表示上一天不进行操作,dp[i-1][1]+prices[i]表示上一天进行了买入当天进行卖出,若收益大于上一天收益则执行该操作
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<=1) return 0;
int n = prices.size();
int dp[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1 ;i<n ;++i){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = dp[i][0] - prices[i];
}
return dp[n-1][0];
}
};
714. 买卖股票的最佳时机含手续费
无限制买卖次数
//含有手续费用需要考虑的不能像之前一样进行频繁买卖。
//dp[i][0]表示第 i 天交易完后手里没有股票的最大利润
//dp[i][1]表示第 i 天交易完后手里持有一支股票的最大利润
//第i天持有股票情况可能是上一天已经持有了股票或者上一天未持有股票当天买入股票两种情况
class Solution{
public:
int maxProfit(vector<int> &prices ,int fee){
int n = prices.size();
vector<vector<int>>dp(n,{0,0});
dp[0][0] = 0; dp[0][1] = -prices[0];
for(int i = 1 ; i<n; ++i){
dp[i][0] = max(dp[i-1][0],dp[i-1][1] +prices[i]-fee);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[n-1][0];
}
};
最长递增序列问题
该类问题可以用二分查找来进行优化
300. 最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
/*
dp[i]表示前i项的最大子序列长度。
解题方案:看到最长序列以及对字符串进行增删改操作此类型的题首先考虑动态规划
本题中考虑一个问题,dp[i]与之前状态有什么关系,如果第i项大于第j项,那么前i项最大子序列的一种情况便是前j项的最大子序列长度加上1。所以考虑递推式dp[i] >= dp[j]+1;所以我计算0-i-1所有能满足第i项大于第j项的项,dp[i]一定等于max(dp[j])+1(其中j满足nums[j]<nums[i]);即递推通项为:
{
当nums[i]>nums[j]时候 dp[i] = max(dp[i],dp[j]+1)
因为最小子串长度为1 所以初始化dp[i] = 1;
}
*/
class Solution{
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if(n <= 1){
return n;
}
vector<int>dp(n,0);
for(int i = 0;i< n;++i){
dp[i] = 1;
for(int j = 0;j< i ;++j){
if(nums[i]>nums[j]){
dp[i] = max(dp[i],dp[j]+1);
}
}
}
int res =0 ;
for(int i =0 ;i< n ;++i){
res = max(res ,dp[i]);
}
return res;
}
};
354. 俄罗斯套娃信封问题
//最长递增子序列变种问题,将数组进行排序,宽从小到大排列,高从大到小排列或从小到大均可,因为需要都遍历一遍。
//因为宽度一样优先选择更高的
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
int n = envelopes.size();
vector<int>dp(n,1);
//相对于300题本题需要排序原因是无法保证第i项为结尾时前i项中包含所有数据,因为第i项后面可能存在数据也能满足,排序后逻辑与300题完全一致
sort(envelopes.begin(), envelopes.end(), [](const auto& e1, const auto& e2) {
return e1[0] < e2[0] || (e1[0] == e2[0] && e1[1] > e2[1]);
});
for(int i = 0;i< n; ++i){
for(int j = 0; j< i ;++j){
if(envelopes[i][0]>envelopes[j][0]&&envelopes[i][1]>envelopes[j][1]){
dp[i] = max(dp[i],dp[j]+1);
}
}
}
return *max_element(dp.begin(),dp.end());
}
};
最大子序列和问题
53. 最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
//dp[i]表示前i项最大和,dp[i] = max(nums[i],dp[i-1]+nums[i]);
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int>dp(nums.size(),0);
dp[0] = nums[0];
for(int i = 1;i<nums.size();++i)
{
dp[i] = max(nums[i],dp[i-1]+nums[i]);
}
return *max_element(dp.begin(),dp.end());
}
};
//定义一个和为sum,sum为累加和,当sum<0时候,此时最大和一定出现在第i项后,另起炉灶,令sum = 0。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int maxsum = INT_MIN;
int sum = 0;
for(int i = 0;i<nums.size();++i)
{
sum+= nums[i];
if(sum>maxsum)
{
maxsum = sum;
}
if(sum<0) sum = 0;
}
return maxsum;
}
};
918. 环形子数组的最大和
给定一个由整数数组 A 表示的环形数组 C,求 C 的非空子数组的最大可能和。
在此处,环形数组意味着数组的末端将会与开头相连呈环状。(形式上,当0 <= i < A.length 时 C[i] = A[i],且当 i >= 0 时 C[i+A.length] = C[i])
此外,子数组最多只能包含固定缓冲区 A 中的每个元素一次。(形式上,对于子数组 C[i], C[i+1], …, C[j],不存在 i <= k1, k2 <= j 其中 k1 % A.length = k2 % A.length)
//该题是最大子序列和的变种问题
//考虑两类情况,跨越边境和不跨越边境。不跨越边境下直接按照最大子序列和来求解。跨越边境下考虑在数组中找到最小子子序和minsum,其最大值为sum-minsum。比较两类情况得到最大值。
class Solution {
public:
int maxSubarraySumCircular(vector<int>& A) {
int n = A.size();
if(n == 1){
return A[0];
}
int maxres = INT_MIN;
int sum = 0;
int Asum = 0;
for(int i = 0;i< n ;++i){
Asum+=A[i];
sum+=A[i];
if(sum>maxres){
maxres = sum;
}
if(sum<0){
sum = 0;
}
}
int minres = INT_MAX;
sum = 0;
for(int i = 0; i< n;++i){
if(sum>0){
sum = 0;
}
sum+=A[i];
if(sum<minres){
minres = sum;
}
}
if(minres == Asum){
return maxres;
}
return max(maxres,Asum-minres);
}
};
152. 乘积最大子数组
//暴力解法
//由于会出现负数,dp数组需要保存两个状态,最大乘积(正)以及最小乘积(负)。
// 最大乘积dp[i][0] = if(nums[i]>=0) :dp[i-1][0]*nums[i] or nums[i](防止0) else dp[i-1][1]*nums[i];
//即 dp[i][0] = max(dp[i-1][0]*nums[i],max(dp[i-1][1]*nums[i],nums[i]);
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
if(n <= 1){
return nums[0];
}
int maxres = nums[0];
for(int i = 0;i< n ;++i){
int res = nums[i];
for(int j = i-1 ;j>=0;--j){
maxres = max(max(nums[i],nums[j]*res),maxres);
res*=nums[j];
}
}
return maxres;
}
};
//动态规划
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
if(n <= 1){
return nums[0];
}
vector<vector<int>>dp(n,vector<int>{0,0});
if(nums[0]>0){
dp[0][0] = nums[0];
}else{
dp[0][1] = nums[0];
}
for(int i = 1;i< n ;++i){
dp[i][0] = max(dp[i-1][0]*nums[i],max(dp[i-1][1]*nums[i],nums[i]));
dp[i][1] = min(dp[i-1][1]*nums[i],min(dp[i-1][0]*nums[i],nums[i]));
/*
if(nums[i]>=0){
dp[i][0] = max(dp[i-1][0]*nums[i],nums[i]);
dp[i][1] = min(dp[i-1][1]*nums[i],0);
}
else{
dp[i][0] = max(dp[i-1][1]*nums[i],0);
dp[i][1] = min(dp[i-1][0]*nums[i],nums[i]);
}*/
}
int maxres = 0;
for(int i = 0;i< n ;++i){
maxres = max(maxres,dp[i][0]);
}
return maxres;
}
};
//由于dp[i]仅与dp[i-1]有关,优化空间复杂度得到:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
if(n <= 1){
return nums[0];
}
int minelement = min(nums[0],0);
int maxelement = max(nums[0],0);
int ans = maxelement;
for(int i = 1;i< n ;++i){
int mx = maxelement, mi = minelement;
maxelement = max(mx*nums[i],max(mi*nums[i],nums[i]));
minelement = min(mi*nums[i],min(mx*nums[i],nums[i]));
ans = max(maxelement,ans);
}
return ans;
}
};
813. 最大平均值和的分组
我们将给定的数组 A
分成 K
个相邻的非空子数组 ,我们的分数由每个子数组内的平均值的总和构成。计算我们所能得到的最大分数是多少。
//dp[i][j]表示为A的前i项备分割为K份下最大分数。
//初始化dp[i][1] = sum(A[:i]);
//建立数组sum[i]保存A数组的前i项和;
//dp[i][j] 若由dp[i-1][j-1]推出:dp[i][j] = dp[i-1][j-1]+A[i]/1;
//dp[i][j] 与dp[i-t][j-1]的关系是dp[i][j] = dp[i-t][j-1]+A[i]/t;
//要保证dp[i][j]最大,dp[i][j] = max(dp[1:t][j-1]+(sum[t:i])/(t:i));
class Solution {
public:
double largestSumOfAverages(vector<int>& A, int K) {
int n = A.size();
vector<vector<double>> dp(n+1,vector<double>(K+1,0));
vector<double> sum(n+1,0);
for(int i = 1;i<= n ;++i){
sum[i]=sum[i-1]+A[i-1];
dp[i][1] = sum[i]/i;
}
for(int i = 1;i<= n ;++i){
for( int j = 2 ;j <= K&&j <= i ;++j){
double temp = 0;
for( int t = i-1 ;t > 0 ;--t){
dp[i][j] = max(dp[i][j],dp[t][j-1]+(sum[i]-sum[t])/(i-t));
}
}
}
return dp[n][K];
}
};
打家劫舍系列
198. 打家劫舍
不可以连续抢劫
//显然dp[i] 表示抢劫前i家最大收益,小偷到第i家时候可以选择偷或者不偷,偷:dp[i] = dp[i-2]+nums[i]; 不偷dp[i] = dp[i-1],递推式很容易得到dp[i] = max(dp[i-2]+nums[i],dp[i-1])
class Solution {
public:
int rob(vector<int>& nums) {
int n= nums.size();
if(n == 0){
return 0;
}
if(n == 1){
return nums[0];
}
vector<int>dp (n,0);
dp[0] = nums[0];
dp[1] = max(dp[0],nums[1]);
for(int i = 2;i< n ;++i){
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[n-1];
}
};
213. 打家劫舍 II
将第一个问题中的房屋设置为环形,即首尾相连
//分两类情况讨论,偷第一家还是偷最后一家,偷第一家,偷的范围为[1,n-1];偷最后一家范围为[0,n-2];对年内情况分别进行动态规划,答案为max(dp1[n-1].dp2[n-2]);
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size(),ans;
if(n == 0) return 0;
if(n == 1) return nums[0];
int dp[n+1];
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2;i<n;i++)
{
dp[i] = max(dp[i-1],dp[i-2] + nums[i-1]);
}
ans = dp[n-1];
dp[1] = nums[1];
for(int i = 2;i<n;i++)
{
dp[i] = max(dp[i-1],dp[i-2] + nums[i]);
}
ans = max(dp[n-1],ans);
return ans;
}
};
1388. 3n 块披萨
复杂化的环形打家劫舍问题
//披萨为3n份,且披萨为首位相连的环,当选择i时候下一步不能选择i-1和i+1,即不能选择相邻的,与打家劫舍类似
//同样带环分为两类问题,过点slices[3n-1]和过slices[0]的情况分开。
//对两个情况打家劫舍得到结果
class Solution {
public:
int calculate(const vector<int>& slices) {
int n = slices.size();
int choose = (n + 1) / 3;
vector<vector<int>> dp(n + 1, vector<int>(choose + 1,0));
dp[1][1] = slices[0];
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= choose; ++j) {
dp[i][j] = max(dp[i - 1][j], dp[i - 2][j - 1] + slices[i - 1]);
}
}
return dp[n][choose];
}
int maxSizeSlices(vector<int>& slices) {
vector<int> v1(slices.begin() + 1, slices.end());
vector<int> v2(slices.begin(), slices.end() - 1);
int ans1 = calculate(v1);
int ans2 = calculate(v2);
return max(ans1, ans2);
}
};
//优化空间复杂度
class Solution {
public:
int maxSizeSlices(vector<int>& slices) {
int n = slices.size();
int choose = n / 3;
int ans = 0;
vector<vector<int>> dp(n + 1, vector<int>(choose + 1,0));
dp[1][1] = slices[0];
for (int i = 2; i < n; ++i) {
for (int j = 1; j <= choose; ++j) {
dp[i][j] = max(dp[i - 1][j], dp[i - 2][j - 1] + slices[i - 1]);
}
}
ans = dp[n-1][choose];
dp[1][1] = slices[1];
for (int i = 2; i < n; ++i) {
for (int j = 1; j <= choose; ++j) {
dp[i][j] = max(dp[i - 1][j], dp[i - 2][j - 1] + slices[i]);
}
}
return max(ans,dp[n-1][choose]);
}
};
256. 粉刷房子
//不能连续选择同一颜色,有点类似打家劫舍。
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
int n= costs.size();
vector<vector<int>>dp(n,vector<int>(3,0));
dp[0][0] = costs[0][0];
dp[0][1] = costs[0][1];
dp[0][2] = costs[0][2];
for(int i = 1;i<n ;++i){
dp[i][0] = min(dp[i-1][1],dp[i-1][2])+costs[i][0];
dp[i][1] = min(dp[i-1][0],dp[i-1][2])+costs[i][1];
dp[i][2] = min(dp[i-1][0],dp[i-1][1])+costs[i][2];
}
return min(min(dp[n-1][0],dp[n-1][1]),dp[n-1][2]);
}
};
子集问题
1027. 最长等差数列
//hash表记录每个数出现的位置
//dp[i][j]表示以i、j作为最后两位的等差数列长度。
//当-A[j]+2*A[i]存在说明,A[idx] = 2*A[i]-A[j];
//等差数列存在dp[i][j] = A[idx][i]+1;
//为了避免重复元素所以遍历完毕小于i的情况下将将A[i]位置放入map中:map[A[i]] = i;
class Solution {
public:
int longestArithSeqLength(vector<int>& A) {
unordered_map<int, int> map;
int n = A.size();
vector<vector<int>> dp(n, vector<int>(n, 2));
int ans = 0;
for (int i = 0; i < n; i ++) {
for (int j = i + 1; j < n; j ++) {
int target = 2 * A[i] - A[j];
if (map.count(target)) dp[i][j] = dp[map[target]][i] + 1;
ans = max(ans, dp[i][j]);
}
//防止出现重复元素
map[A[i]] = i;
}
return ans;
}
};
368. 最大整除子集
//i:排个序,因为保证的每个元素对互相整除,所以要从大到小
//if(nums[i]%nums[j] == 0) 说明nums[i]、nums[j]满足整除,得dp[i] = dp[j]+1;
//由于需要返回数组,建立数组将位置进行储存,当该数组是最大长度时,将最大的i赋给end,last[i]中储存的是上一个整除数得位置。
class Solution {
public:
vector<int> largestDivisibleSubset(vector<int>& nums) {
int sz = nums.size(),mx = 0,end = -1;
vector<int> dp(sz,1),last(sz,-1),res;
sort(nums.begin(),nums.end());
for(int i = 0;i<sz;i++){
for(int j = 0;j<i;j++){
if(nums[i]%nums[j] == 0 && dp[i]<=dp[j]){
dp[i] = dp[j]+1;
last[i] = j;
}
}
if(dp[i]>mx){
mx = dp[i];
end = i;
}
}
for(int i = end;i!=-1;i = last[i]){//倒序输出
res.push_back(nums[i]);
}
return res;
}
};
413. 等差数列划分
//建立dp[i],i为以i为结尾的等差数列个数
//if(nums[i]-nums[i-1] == nums[i-1] - nums[i-2]) dp[i] = dp[i-1]+1;
//递推结束遍历dp,其和为结果。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n = nums.size();
if(n<3) return 0;
vector<int> dp(n,0);
int sum = 0;
for(int i = 2;i< n;++ i){
if(nums[i]-nums[i-1] == nums[i-1] - nums[i-2]){
dp[i] = dp[i-1]+1;
sum+=dp[i];
}
}
return sum;
}
};
873. 最长的斐波那契子序列的长度
如果序列 X_1, X_2, …, X_n 满足下列条件,就说它是 斐波那契式 的:
n >= 3
对于所有 i + 2 <= n,都有 X_i + X{i+1} = X{i+2}
给定一个严格递增的正整数数组形成序列,找到 A
中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
//类似1027最长等差数列问题
//hash表记录每个数出现的位置
//dp[i][j]表示以i、j作为最后两位的斐波那契式的长度。
//当arr[j] - arr[i]出现时,说明存在连续斐波那契式,arr[idx] = arr[j-arr[i]].
//此时dp[i][j] = max(dp[i][j],dp[idx][i]+1);当dp[i][j]>=1时候说明存在,长度为dp[i][j]+2;
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n = arr.size();
unordered_map<int, int> hash;
vector<vector<int>> f(n, vector<int>(n));
int ans = 0;
for (int i = 0; i < n; ++i) hash[arr[i]] = i;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (hash.count(arr[j] - arr[i]) && (arr[j] - arr[i] < arr[i])) {
auto idx = hash[arr[j] - arr[i]];
f[i][j] = max(f[i][j], f[idx][i] + 1);
ans = max(ans, f[i][j] + 2);
}
}
}
if (ans < 3) return 0;
return ans;
}
};
公共子序列问题
1143. 最长公共子序列
//最长公共子序列dp[i][j]。i为text1前i个数,j为text2前j个数 。
//初始化dp[i][0]和dp[0][j]。
//if(text1[i]!= text2[j]) dp[i][j] = dp[i-1][j-1];
//else dp[i][j] = dp[i-1][j] or dp[i][j-1] dp只可能是递增的。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size();
int m = text2.size();
vector<vector<int>> dp (n,vector<int>(m,0));
int flag = false;
for(int i = 0;i <n ;++i){
if(text1[i] == text2[0]){
flag = true;
}
if(flag){
dp[i][0] = 1;
}
}
flag = false;
for(int i = 0;i <m ;++i){
if(text2[i] == text1[0]){
flag = true;
}
if(flag){
dp[0][i] = 1;
}
}
for(int i = 1;i <n ;++i){
for(int j = 1; j< m ;++j){
if(text1[i] == text2[j]){
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-1][m-1];
}
};
//稍微优化一下
//优化后不需要考虑初始问题
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size();
int m = text2.size();
vector<vector<int>> dp (n+1,vector<int>(m+1,0));
for(int i = 1;i <= n ;++i){
for(int j = 1; j<= m ;++j){
if(text1[i-1] == text2[j-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];
}
};
712. 两个字符串的最小ASCII删除和
给定两个字符串s1, s2
,找到使两个字符串相等所需删除字符的ASCII值的最小和。
//最长公共子序列问题
//本质上是找到最长子序列,在寻找过程中选择最小的删除方案进行处理,记录每一个删除的ASCII大小,这个需要初始化
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int len1 = s1.size();
int len2 = s2.size();
vector<vector<int>> dp(len1+1,vector<int>(len2+1,0));
for(int i = 1;i<= len1; ++i){
dp[i][0] = dp[i-1][0]+s1[i-1];
}
for(int i = 1;i<= len2; ++i){
dp[0][i] = dp[0][i-1]+s2[i-1];
}
for(int i = 1;i<= len1; ++i){
for(int j = 1; j<=len2 ;++j){
if(s1[i-1] == s2[j-1]){
dp[i][j] = dp[i-1][j-1];
}
else{
//不相同需要删除一个元素,选择从两条路径选择最短删除
dp[i][j] = min(dp[i-1][j]+s1[i-1],dp[i][j-1]+s2[j-1]);
}
}
}
return dp[len1][len2];
}
};
718. 最长重复子数组
//与题1143的区别在于子数组与子序列区别是子数组必须是连续的不间断的
//dp[i][j]定义为以i,j为结尾的数组的最长重复子串
//if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
//else 另起炉灶 dp[i][j] = 0;
//遍历一遍,找到最大的dp即可
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int len2 = nums2.size();
int ans = 0;
vector<vector<int>> dp (len1+1,vector<int>(len2+1,0));
for(int i = 1 ;i<=len1 ;++i){
for(int j = 1 ;j<= len2 ;++j){
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}
else{
dp[i][j] = 0;
}
ans = max(ans,dp[i][j]);
}
}
return ans;
}
};
97. 交错字符串
//dp[i][j]是前i项和前j项能不能交错成为S3的前i+j项
//dp[0][0] = true;
//if(s1[i-1] == s3[i+j-1]) dp[i][j] = dp[i-1][j];
//if(s2[j-1] == s3[i+j-1]) dp[i][j] = dp[i][j-1];
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int len1 = s1.size();
int len2 = s2.size();
if(len1 + len2 != s3.size()){
return false;
}
vector<vector<int>>dp(len1+1,vector<int>(len2+1,0));
dp[0][0] = true;
for(int i = 1;i<= len1 ;++i){
if(s1.substr(0,i) == s3.substr(0,i)){
dp[i][0] = true;
}
}
for(int i = 1;i<= len2 ;++i){
if(s2.substr(0,i) == s3.substr(0,i)){
dp[0][i] = true;
}
}
for(int i = 1;i<= len1 ;++i){
for(int j = 1;j<= len2 ;++j){
if(s1[i-1] == s3[i-1+j]&&dp[i-1][j]){
dp[i][j] = true;
}
if(s2[j-1] == s3[i-1+j]&&dp[i][j-1]){
dp[i][j] = true;
}
}
}
return dp[len1][len2];
}
};
//完全可以省略初始化情况
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int len1 = s1.size();
int len2 = s2.size();
if(len1 + len2 != s3.size()){
return false;
}
vector<vector<int>>dp(len1+1,vector<int>(len2+1,0));
dp[0][0] = true;
for(int i = 0;i<= len1 ;++i){
for(int j = 0;j<= len2 ;++j){
if(i>0&&s1[i-1] == s3[i-1+j]&&dp[i-1][j]){
dp[i][j] = true;
}
if(j>0&&s2[j-1] == s3[i-1+j]&&dp[i][j-1]){
dp[i][j] = true;
}
}
}
return dp[len1][len2];
}
};
583. 两个字符串的删除操作
//最长公共子序列问题
//删除字符串不就是找到两个字符串的最大子序列吗,删除大小为n+m- 2*maxlen;
class Solution {
public:
int minDistance(string text1, string text2) {
int n = text1.size();
int m = text2.size();
vector<vector<int>> dp (n+1,vector<int>(m+1,0));
for(int i = 1;i <= n ;++i){
for(int j = 1; j<= m ;++j){
if(text1[i-1] == text2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}
else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
return n+m-2*dp[n][m];
}
};
115. 不同的子序列
给定一个字符串 s
和一个字符串 t
,计算在 s
的子序列中 t
出现的个数。
//dp[i][j]表示前i,j项
//if(s[i-1] != t[j-1]) dp[i][j]中末尾不是t[j-1]所以与上一个状态前(i-1)项出现t的前j项情况一致dp[i-1][j]。
//if(s[i-1] == t[j-1]) dp[i][j]有两种选择dp[i-1][j]和dp[i][j-1]都是其子情况
class Solution {
public:
int numDistinct(string s, string t) {
int n = s.size();
int m = t.size();
vector<vector<unsigned long long>>dp(n+1,vector<unsigned long long>(m+1,0));
dp[0][0] =1;
int a = 0;
for(int i = 1 ;i<= n ;++i){
dp[i][0] = 1;
}
for(int i = 1; i<= n;++i){
for(int j = 1; j<= m&&j<=i ;++j){
if(s[i-1] != t[j-1]){
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = dp[i-1][j]+dp[i-1][j-1];
}
}
}
return (int)dp[n][m];
}
};
72. 编辑距离
//当(word1[i] == word2[j])时候 dp[i][j] = dp[i-1][j-1];(即不做增删改)
//word1[i]!=word2[j]时 dp[i][j] 有三种选择 dp[i-1][j]+1:添加一个, dp[i][j-1]+1删除一个,dp[i-1][j-1]+1更改一个,选择最小情况。
class Solution {
public:
int minDistance(string word1, string word2) {
int len1 = word1.size();
int len2 = word2.size();
vector<vector<int>> dp(len1+1,vector<int>(len2+1,0));
for(int i = 0;i< len1+1 ;++i){
dp[i][0] = i;
}
for(int i = 0;i< len2+1 ;++i){
dp[0][i] = i;
}
for(int i = 1;i< len1+1 ;++i){
for(int j = 1;j< len2+1 ;++j){
if(word1[i-1] == word2[j-1]){
dp[i][j] = dp[i-1][j-1];
}
else{
dp[i][j] = 1+min(min(dp[i-1][j-1],dp[i][j-1]),dp[i-1][j]);
}
}
}
return dp[len1][len2];
}
};
字符串匹配
44. 通配符匹配
考虑一个问题dp[i] [j]
什么时候用来表示前i、j
项,什么时候表示以i、j
为结尾
?可以匹配任意字符,*可以匹配任意字符串(含空串)
//dp[i][j]表示数组的前i,j项是否可以被匹配。
//if(p[j] == '?'||p[j] == s[i]) i、j位置一定被匹配: dp[i][j] = dp[i-1][j-1];
//if(p[j] == '*') 两种情况:一是看成空串 dp[i][j] = dp[i][j-1];
// 二是看成任意字符串(将S[i]位置进行匹配)dp[i][j] = dp[i-1][j];
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
dp[0][0] = true;
for(int i = 1;i<= n ;++i){
dp[0][i] = dp[0][i-1]&&(p[i-1] == '*');
}
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
//当为?时说明当前项一定匹配成功
if(p[j-1] == '?'||p[j-1] == s[i-1]){
dp[i][j] = dp[i-1][j-1];
}
//当为*时候,dp[i][j]可以选择要该项或者不要
//要是dp[i][j] = dp[i-1][j];不要为dp[i][j-1];
if(p[j-1] == '*'){
dp[i][j] = dp[i-1][j]||dp[i][j-1];
}
}
}
return dp[m][n];
}
};
剑指 Offer 19. 正则表达式匹配
匹配包含'. '
和'*'
的正则表达式
模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(含0次)。
//dp[i][j]表示数组的前i,j项是否可以被匹配。
//if(p[j] == '.'||p[j] == s[i]) i、j位置一定被匹配: dp[i][j] = dp[i-1][j-1];
//if(p[j] == '*') 两种情况 一是看成空串(前一字符出现0次) dp[i][j] = dp[i][j-2];
// 二是看成前一字符串进行匹配 if(s[i] == p[j-1]||p[j-1] == '.') 说明匹配成功dp[i][j] = dp[i-1][j]
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size() + 1, n = p.size() + 1;
vector<vector<bool>> dp(m, vector<bool>(n, false));
dp[0][0] = true;
// 初始化首行
for(int j = 2; j < n; j += 2)
dp[0][j] = dp[0][j - 2] && p[j - 1] == '*';
// 状态转移
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
if(p[j - 1] == '*') {
//不看*的情况
//由于*不可能出现在第一位所以不会出现越界
if(dp[i][j - 2]) dp[i][j] = true;
// 看*的情况分为两类,一类是s[i-1] == p[j-2]此时说明第i项存在匹配,接着判断第i-1项是否匹配
else if(dp[i - 1][j] && s[i - 1] == p[j - 2]) dp[i][j] = true;
else if(dp[i - 1][j] && p[j - 2] == '.') dp[i][j] = true;
} else {
if(dp[i - 1][j - 1] && s[i - 1] == p[j - 1]) dp[i][j] = true;
else if(dp[i - 1][j - 1] && p[j - 1] == '.') dp[i][j] = true;
}
}
}
return dp[m - 1][n - 1];
}
};
背包问题专栏
416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
//直接采用0-1背包二维解法
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0,maxnumber = 0, n = nums.size();
if (n < 2) {
return false;
}
for(int i = 0;i< n ;++i){
sum+=nums[i];
maxnumber = max(maxnumber,nums[i]);
}
int target = sum/2;
//当元素和为奇数或者最大元素大于和的一半时,不可能存在等和子集
if(sum%2!=0||maxnumber >target) {
return false;
}
if(maxnumber == target){
return true;
}
vector<vector<int>>dp(n+1,vector<int>(target+1,0));
//将题目转化为背包容量为sum/2的背包问题,看当背包容量为sum/2下背包元素和最大是否存在等于sum/2。
for(int i = 0;i<=n;++i){
for(int j = target ; j>=0;--j){
if(j>=nums[i]){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
}
else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][target] == target;
}
};
//将二维DP简化为一维DP问题
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0,maxnumber = 0, n = nums.size();
if (n < 2) {
return false;
}
for(int i = 0;i< n ;++i){
sum+=nums[i];
maxnumber = max(maxnumber,nums[i]);
}
int target = sum/2;
if(sum%2!=0||maxnumber >target) {
return false;
}
//将二维DP转化为一维DP,因为二维DP公式只与DP[i-1]状态有关,故可以合并。但是一定考虑需要逆序遍历DP。
vector<int>dp(target+1,0);
for(int i = 1;i<=n;++i){
for(int j = target ; j>=nums[i-1];--j){
dp[j] = max(dp[j],dp[j-nums[i-1]]+nums[i-1]);
}
}
return dp[target] == target;
}
};
//背包问题变种,因为是考虑是否存在平衡数,不需要进行考虑,
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
for (auto& num : nums) {
sum += num;
maxNum = max(maxNum, num);
}
if (sum & 1) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
vector<int> dp(target + 1, 0);
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
//当存在数组之和为j时,dp[j] = true;
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
}
};
474. 一和零
class Solution {
private:
int check0(string s){
int res = 0;
for(int i = 0;i< s.size() ;++i){
if(s[i] == '0') res++;
}
return res;
}
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
int strsize = strs.size();
for(int i = 0;i< strsize;++i){
int zeros = check0(strs[i]);
int ones =strs[i].size()-zeros;
for(int j = m;j>=zeros ;--j){
for(int k = n; k>=ones;--k){
dp[j][k] = max(dp[j][k],dp[j-zeros][k-ones]+1);
}
}
}
return dp[m][n];
}
};
494. 目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
X
−
Y
=
S
;
X
+
Y
=
s
u
m
;
X
=
(
S
+
s
u
m
)
/
2
X-Y=S; X+Y=sum; X = (S+sum)/2
X−Y=S;X+Y=sum;X=(S+sum)/2
//设添加符号完成后数组中正整数和为X,负数和的绝对值为Y。原数组数据和为sum,目标值为S,得:
//X-Y=S,X+Y=sum,X=(S+sum)/2;
//由上可看出该题转化为在数组中找到和为(S+sum)/2的数组。显然该题转化为0-1背包问题,背包大小为(S+sum)/2,背包中每个元素Values与其消耗空间相同。
//易得递推公式为dp[j] = dp[j]+dp[j-nums[i]];
//递推原理,递推中该次情况由两种情况推出,即该次不向背包添加数据(上轮dp[j])以及向背包添加数据(上轮dp[j-nums[i]]);
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
int n = nums.size();
int sum = 0;
for(int i = 0;i<n;++i){
sum+=nums[i];
}
if(abs(S)>sum||(S+sum)%2!=0){
return false;
}
int target = (S+sum)/2;
vector<int> dp(target+1,0);
dp[0] =1;
for(int i = 0;i<n ;++i){
for(int j = target ;j>=nums[i] ;--j){
dp[j] = dp[j]+dp[j-nums[i]];
}
}
return dp[target];
}
};
879. 盈利计划
集团里有 n 名员工,他们可以完成各种各样的工作创造利润。
第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。
工作的任何至少产生 minProfit 利润的子集称为盈利计划。并且工作的成员总数最多为 n 。
有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值。
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
int len = group.size();
const int M = 1e9+7;
vector<vector<int>>dp(n+1,vector<int>(minProfit+1,0));
for (int j = 0; j <= n; ++j) {
dp[j][0] = 1;
}
for(int i = 0;i<len ;++i){
int g = group[i];
int p = profit[i];
for(int j = n; j>=g ;--j){
for(int k = minProfit ;k>=0 ;--k){
dp[j][k]+=dp[j-g][max(k-p,0)];
dp[j][k]%=M;
}
}
}
return dp[n][minProfit];
}
};
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
//dp[j]代表含义:填满容量为j的背包最少需要多少硬币
//初始化dp数组:因为硬币的数量一定不会超过amount,所以初始化数组为amount+1;dp[0] = 0;
//转移方程:dp[j] = min(dp[j], dp[j - coins[i]] + 1)当前填满容量j最少需要的硬币 = min( 之前填满容量j最少需要的硬币, 填满容量 j - coin 需要的硬币 + 1个当前硬币)
//返回dp[amount],如果dp[amount]的值为10001没有变过,说明找不到硬币组合,返回-1
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for(int i = 0; i< coins.size() ;++i){
for(int j = coins[i];j<=amount ;++j){
dp[j] = min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
518. 零钱兑换 II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
//完全背包问题,因为可重复使用硬币
//若为二维dp,dp[i][j] = dp[i-1][j]+dp[i-1][j-coins[i]];因为当前硬币选择数等于该轮不选硬币以及该轮选择硬币的方案之和
class Solution {
public:
int change(int amount, vector<int>& coins) {
int len = coins.size();
int minnumber = 0;
for(int i = 0;i<len ;++i){
minnumber = min(minnumber,coins[i]);
}
if(minnumber>amount){
return 0;
}
vector<int>dp(amount+1,0);
dp[0] = 1;
for(int i =0;i<len ;++i){
for(int j = coins[i] ; j<=amount ; ++j){
dp[j] = dp[j]+dp[j-coins[i]];
}
}
return dp[amount];
}
};
449. 数位成本和为目标值的最大数字
给你一个整数数组 cost 和一个整数 target 。请你返回满足如下规则可以得到的 最大 整数:
给当前结果添加一个数位(i + 1)的成本为 cost[i] (cost 数组下标从 0 开始)。
总成本必须恰好等于 target 。
添加的数位中没有数字 0 。
由于答案可能会很大,请你以字符串形式返回。
如果按照上述要求无法得到任何整数,请你返回 “0” 。
//本题为完全背包问题中将背包恰好装满模型,问题在于如何处理字符串比较问题。其余与完全背包完全一致
class Solution {
public:
//string dp[9 + 5][5000 + 5];
// 返回两者较大的一个
string string_max(const string &lhs, const string &rhs) {
if (lhs.size() > rhs.size()) return lhs;
if (rhs.size() > lhs.size()) return rhs;
// 当两字符串长度相等时
if (lhs > rhs) return lhs;
else return rhs;
}
string largestNumber(vector<int> cost, int target) {
int len = cost.size();
vector<string>dp(target+1,"#");
dp[0] = "";
for (int i = 0; i < len; ++i) {
for (int j = cost[i]; j <= target; ++j) {
string a, b;
// 不选第i个
a = dp[j];
// 加选一个
if (dp[j - cost[i]] != "#")
b = to_string(i+1) + dp[j - cost[i]];
dp[j] = string_max(a, b);
}
}
if (dp[target] == "#") return "0";
else return dp[target];
}
};
满减优惠券(非力扣题)
给定一张满减优惠券,问在购物车内如何选择可以使得优惠券力度最大
路径规划问题
45. 跳跃游戏 II
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
//方法一:动态规划 复杂度较大
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,INT_MAX);
dp[0] = 0;
for(int i = 1;i< n;++i){
for(int j = 0; j<i ;++j){
if(nums[j]+j >= i){
dp[i] = min(dp[i],dp[j]+1);
}
}
}
return dp[n-1];
}
};
//贪心算法,动态规划中记录每个位置到该点的情况非常多余,因为考虑到一定会到最后一点,只需要记录前i项可以跳到的最远距离即可。当我走到第i项时候,直接选择跳到最远距离重新开始向前走。
class Solution {
public:
int jump(vector<int>& nums) {
int maxPos = 0, n = nums.size(), end = 0, step = 0;
for (int i = 0; i < n - 1; ++i) {
//maxpos最大距离
if (maxPos >= i) {
maxPos = max(maxPos, i + nums[i]);
if (i == end) {
//下一次起跳点
end = maxPos;
++step;
}
}
}
return step;
}
};
63. 不同路径 II
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int row = obstacleGrid.size();
int col = obstacleGrid[0].size();
vector<vector<int>> dp(row,vector<int>(col,0));
if(obstacleGrid[0][0] == 0)dp[0][0] = 1;
for(int i = 1;i<row ;++i){
dp[i][0] = dp[i-1][0];
if(obstacleGrid[i][0] == 1){
dp[i][0] = 0;
}
}
for(int i = 1;i< col;++i){
dp[0][i] = dp[0][i-1];
if(obstacleGrid[0][i] == 1){
dp[0][i] = 0;
}
}
for(int i = 1 ;i< row ;++i){
for(int j = 1; j< col ;++j){
if(obstacleGrid[i][j] == 1){
continue;
}else{
dp[i][j] = dp[i-1][j] +dp[i][j-1];
}
}
}
return dp[row-1][col-1];
}
};
403. 青蛙过河
检测青蛙是否可以过河,如果青蛙上一步跳跃了 k
个单位,那么它接下来的跳跃距离只能选择为 k - 1
、k
或 k + 1
个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
class Solution {
public:
bool canCross(vector<int>& stones) {
int n = stones.size();
if(n<=1) return true;
unordered_map<int,int> map;
vector<vector<int>>dp(n+1,vector<int>(n+1,0));
dp[0][0] = 1;
for(int i = 0;i<n ;++i){
map[stones[i]] = i;
}
if(stones[1] > 1){
return false;
}
dp[1][1] = true;
for(int i = 2;i< n ;++i){
for(int k = 1 ;k<= i ;++k){
if(map.count(stones[i]-k)>0){
int j = map[stones[i]-k];
dp[i][k] = dp[j][k]||dp[j][k-1]||dp[j][k+1];
}
}
}
for(int i = 0;i< n ;++i){
if(dp[n-1][i] == true) return true;
}
return false;
}
};
64. 最小路径和
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int n = grid.size();
int m = grid[0].size();
vector<vector<int>>dp(n,vector<int>(m,0));
dp[0][0] = grid[0][0];
for(int i = 1;i<n ;++i){
dp[i][0] = dp[i-1][0]+grid[i][0];
}
for(int i = 1;i<m ;++i){
dp[0][i] = dp[0][i-1]+grid[0][i];
}
for(int i = 1 ;i< n ;++i){
for(int j = 1; j < m ;++j){
dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
return dp[n-1][m-1];
}
};
其他
871. 最低加油次数
class Solution {
public:
int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
int n = stations.size();
vector<long long>dp(n+1,0);
vector<bool>flag(n,false);
dp[0] = startFuel;
for(int i = 1 ;i< n+1;++i){
int maxfuel = 0;
int pos = -1;
for(int j = 0;j<n&&stations[j][0]<= dp[i-1] ;++j){
if(maxfuel < stations[j][1]&&flag[j] == false){
maxfuel = stations[j][1];
pos = j;
}
}
if(pos>=0) flag[pos] = true;
dp[i] = dp[i-1]+maxfuel;
}
for(int i =0;i<= n ;++i){
if(target<=dp[i]){
return i;
}
}
return -1;
}
};
91. 解码方法
//dp简单,重点在于边界信息的处理
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
vector<int>dp(n,0);
if(s[0] == '0'){
return 0;
}else{
dp[0] = 1;
}
for(int i = 1;i< n;++i){
if(s[i] == '0'){
if(s[i-1] == '2'||s[i-1] == '1'){
if(i>=2)dp[i] = dp[i-2];
else dp[i] = 1;
}else{
return 0;
}
}
else{
if(s[i-1]>'2'||(s[i-1] == '2'&&s[i]>'6')||s[i-1] == '0'){
dp[i] = dp[i-1];
}else{
if(i>=2)dp[i] = dp[i-1]+dp[i-2];
else dp[i] = 2;
}
}
}
return dp[n-1];
}
};
二分法搜索
410. 分割数组的最大值
找使得最大值最小的一般都可以用二分搜索
//动态规划
class Solution {
public:
int splitArray(vector<int>& nums, int m) {
int n = nums.size();
vector<vector<long long>> f(n + 1, vector<long long>(m + 1, LLONG_MAX));
vector<long long> sub(n + 1, 0);
for (int i = 0; i < n; i++) {
sub[i + 1] = sub[i] + nums[i];
}
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= min(i, m); j++) {
for (int k = 0; k < i; k++) {
f[i][j] = min(f[i][j], max(f[k][j - 1], sub[i] - sub[k]));
}
}
}
return (int)f[n][m];
}
};
//*****二分搜索法
class Solution {
bool check(vector<int>& nums , int x ,int m){
long long sum = 0;
//连续和大于目标值的分割个数
int cnt = 1;
for(int i =0;i< nums.size() ;++i){
sum+=nums[i];
if(sum > x){
sum = nums[i];
cnt++;
}
}
//当分割次数小于m,说明这个分割次数太少了,那么说明x太大了,下一步要缩小x
return cnt<=m;
}
public:
int splitArray(vector<int>& nums, int m) {
long long left = 0, right = 0;
int n = nums.size();
for(int i = 0;i< n ;++i){
right+=nums[i];
if(nums[i]>left){
left = nums[i];
}
}
while(left<right){
long long mid = (left+right)>>1;
//当check == true说明mid这个数大了,将搜索范围设置在左侧(较小侧)进行搜索。
if(check(nums,mid,m)){
right = mid;
}
else{
left = mid+1;
}
}
return left;
}
};
LCP 12. 小张刷题计划
class Solution {
bool check(vector<int>& time ,int x ,int m){
int cnt = 1;
long long totaltime = 0;
int maxtime = 0;
for(int i = 0;i< time.size() ;++i){
//将最大值和当前的数进行对比,将较小的添加进入,这样便将整个数组的最大项进行保存并始终使最大项不加入数组和中,得到的数组和是连续数组不包含最大项的数组和
int nexttime = min(maxtime,time[i]);
if(nexttime + totaltime <= x){
totaltime+=nexttime;
//记录当前最大值
maxtime = max(maxtime,time[i]);
}
else{
++cnt;
totaltime = 0;
maxtime = time[i];
}
}
return cnt <= m;
}
public:
int minTime(vector<int>& time, int m) {
int n = time.size();
long long left = 0, right = 0;
for(int i = 0;i< n;++i){
right+=time[i];
}
while(left<right){
int mid = (left+right)>>1;
if(check(time,mid,m)){
right = mid;
}else{
left =mid+1;
}
}
return left;
}
};
887. 鸡蛋掉落
//二分法加动态规划
class Solution {
//由于二分法的递归原因,采用哈希map作为dp。k<=100,所以100*n+k为每个dp[n][k]的对应位置
unordered_map<int, int> map;
int dp(int k, int n) {
//当map中不存在n * 100 + k,表示dp[n][k]尚未被遍历
//n为楼层,k为鸡蛋数量
//对每一个dp[i][j]进行一次二分搜索
if (map.find(n * 100 + k) == map.end()) {
int ans;
if (n == 0) {
ans = 0;
} else if (k == 1) {
ans = n;
} else {
int left = 1, right = n;
while (left + 1 < right) {
int x = (left + right) / 2;
int t1 = dp(k - 1, x - 1);
int t2 = dp(k, n - x);
if (t1 < t2) {
left = x;
} else if (t1 > t2) {
right = x;
} else {
left = right = x;
}
}
ans = 1 + min(max(dp(k - 1, left - 1), dp(k, n - left)),
max(dp(k - 1, right - 1), dp(k, n - right)));
}
map[n * 100 + k] = ans;
}
return map[n * 100 + k];
}
public:
int superEggDrop(int k, int n) {
return dp(k, n);
}
};
1723. 完成所有工作的最短时间
方法1: 暴力回溯
方法2:剪枝
-
对数组进行降序排序
-
如果当前工人未被分配工作,那么下一个工人也必然未被分配工作
或者当前工作恰能使该工人的工作量达到了上限 -
当工人 i 还没被分配工作时,我们不给工人 i+1分配工作
class Solution {
private:
int temp = 0;
bool binarySearch(vector<int>& jobs, int k ,int target){
vector<int> workloads(k,0);
return backtrack(jobs,workloads,0,target);
}
bool backtrack(vector<int>& jobs, vector<int>& workloads, int idx, int limit) {
if (idx >= jobs.size()) {
return true;
}
int cur = jobs[idx];
for(int i = 0;i< workloads.size() ;++i){
if(workloads[i]+cur > limit){
continue;
}
workloads[i]+=cur;
if(backtrack(jobs,workloads,idx+1,limit)){
return true;
};
workloads[i]-=cur;
if (workloads[i] == 0 || workloads[i] + cur == limit) {
break;
}
}
return false;
}
public:
int minimumTimeRequired(vector<int>& jobs, int k) {
int n = jobs.size();
//采用sort排序后可以有效剪枝,因为处理更耗时间的更加
sort(jobs.begin(), jobs.end(), greater<int>());
int left = jobs[0];
int right = 0;
for(int i = 0;i< n ;++i){
right+=jobs[i];
}
while(left<right){
int mid = (left+right)>>1;
if(binarySearch(jobs,k,mid)){
right = mid;
}else{
left = mid+1;
}
}
return left;
}
};