数学归纳法
Step1:验证K0成立
Step2:证明如果K1成立,那么K(i+1)也成立
Step3:联合Step1与Step2,证明由K0->Kn成立
状态转移方程中的重点
状态:一个数学符号,外加一个语义描述
决策:从所有可能产生最优解的状态中,选择一个最大值
阶段:本阶段只依赖上一个阶段。
leetcode-714. 买卖股票的最佳时机含手续费
状态定义:
dp[i][0]代表不持有股票的最大收益
dp[i][1]代表持有股票的最大收益
状态转移方程:
**dp[i][0]来自:**
1.i-1天没买股票、第i天也没买股票;
最大值为dp[i-1][0]
2.i-1天有股票、第i天把股票卖了。
最大值为dp[i-1][1]+price[i]-fee
因此,第i天不持有股票的最大收益在两者之间取个最大值:
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+price[i]-fee)
**dp[i][1]来自:**
1.i-1天就持有股票且第i天也持有股票:
最大值为:dp[i-1][1]
2.i-1天没买股票,第i天买股票了
最大值为dp[i-1][0]-price[i]
因此,第i天持有股票的最大收益在两者之间取个最大值:
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-price[i])
代码演示:
class Solution {
public:
//dp[i][0]表示第i天不持有股票的最大收益 来自1.第i-1天没有股票 2.第i天将股票卖掉
//dp[i][1]表示第i天持有股票的最大收益
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
int dp[n][2];
dp[0][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 max(dp[n-1][0],dp[n-1][1]);//在最后一天持有股票和不持有股票之间的最大值取一个
}
};
leetcode-213. 打家劫舍 II
状态定义:
dp[i][0]表示不偷第i家的最大值
dp[i][1]表示偷第i家的最大值
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
int dp[n][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for(int i=1;i<n;i++){
dp[i][0] = max(dp[i-1][1],dp[i-1][0]);
dp[i][1] = dp[i-1][0]+nums[i];
}
int ans1=dp[n-1][0];//这是选择了最后一个没偷的,第一个偷不偷不重要
dp[0][0] = 0;
dp[0][1] = 0;//这是选择第一个一定不偷 最后一个偷不偷不重要
for(int i=1;i<n;i++){
dp[i][0] = max(dp[i-1][1],dp[i-1][0]);
dp[i][1] = dp[i-1][0]+nums[i];
}
int ans2 = max(dp[n-1][1],dp[n-1][0]);
return max(ans1,ans2);
}
};
leetcode-416. 分割等和子集
可达数组概念:在某种情况下,可以到达某些状态或值的数组称为可达数组
二维状态定义:f[i][j]表示前i个数字是否能凑出j值
状态转移方程:
f[i][j] = f[i-1][j]|f[i-1][j-nums[i]] 两种状态满足一个 f[i][j]状态就等于1
降为一维:
因为f[i][j]只取决于第i-1行第j列和j-x的列(图中两块阴影部分的区域值)
滚动数组可以存两行的值,我们现在可以只存一行
例如:i-1行变成第i行 :i-1行的第j列和j-x列转换成第i行的第j列。因此j将从后往前遍历 可以防止前面的j-x值没有被覆盖
class Solution {
public:
//可达数组:在某种情况下,可以到达某些状态|值
bool canPartition(vector<int>& nums) {
int sum = 0;
for(auto x:nums)sum+=x;
if(sum%2) return false;//奇数一定不能分成两个数组
int f[sum+1];
memset(f,0,sizeof(f));//一开始f数组每个位置都设定为不可达(每个位置拼凑不出来)
f[0] = 1;//初始化0值是可以到达的
sum = 0;//这里表示当前值x之前能拼凑出来的最大值是什么
for(auto x:nums){
sum+=x;
for(int j=sum;j>=x;j--){//倒着扫描一遍
f[j] = f[j]|f[j-x];
}
}
return f[sum/2];
}
};
leetcode-474. 一和零
此题类似于背包问题,原因是固定总资源(背包容量)的大小选择不同的物品集合
dp[i][m][n]前i个字符串,最多m个0和n个1的数量
状态转移方程:
dp[i][m][n] = max(dp[i-1][m][n],dp[i-1][m-1i][n-0i]+1)
这里1i表示第i个字符串中1个数,同理0i。
这里使用的优化方法都是逆向刷表法 可以降维
class Solution {
public:
//此题类似于背包问题,原因是固定总资源(背包容量)的大小选择不同的物品集合
//dp[i][m][n]前i个字符串,最多m个0和n个1的数量
//状态转移方程:
//dp[i][m][n] = max(dp[i-1][m][n],dp[i-1][m-1i][n-0i]+1)
int findMaxForm(vector<string>& strs, int m, int n) {
int dp[m+1][n+1];
memset(dp,0,sizeof(dp));
for(auto x:strs){
int cnt0 = 0,cnt1=0;//计算当前字符串中0的数量和1的数量
for(auto y:x){
if(y == '0') cnt0+=1;
else cnt1+=1;
}
for(int i=m;i>=cnt0;i--){//倒着刷表
for(int j=n;j>=cnt1;j--){
dp[i][j] = max(dp[i][j],dp[i-cnt0][j-cnt1]+1);
}
}
}
return dp[m][n];
}
};
leetcode-494. 目标和
//这里f[i][j]状态定义为前i个数字能达到j的方法总数
//状态转移方程为:
//f[i][j] = f[i-1][j+x]+f[i-1][j-x]; 因为第i个数x可正可负所以应将二者方法数相加
/*这里需要注意:
j在这里可正可负所以不能通过数组来进行存储
解决方法1.可以分成两个数组,正数数组+负数数组 2.可以使用hash表进行存储 3.偏移量
*/
偏移量图解:定义一个比原来sum大两倍加一大小的数组 让p指针指向中间位置,使得向左跑为负数,向右跑为正数,且二者范围都是sum。(可以理解为支持负数下标的数组)
class Solution {
public:
//这里f[i][j]状态定义为前i个数字能达到j的方法总数
//状态转移方程为:
//f[i][j] = f[i-1][j+x]+f[i-1][j-x]; 因为第i个数x可正可负所以应将二者方法数相加
/*这里需要注意:
j在这里可正可负所以不能通过数组来进行存储
解决方法1.可以分成两个数组,正数数组+负数数组 2.可以使用hash表进行存储 3.偏移量
这道题使用了三种优化方法:1偏移量2.滚动数组3.我到哪里去
*/
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0,n=nums.size();
for(auto x:nums)sum+=x;
int buff[2][2*sum+5],*f[2]={buff[0]+sum+2,buff[1]+sum+2};//这里声明的buff数组长度没有特别讲究 不小于2*sum+1即可且稍微大一些 buff的位置就相当于数组的0位置下标
sum = 0;
memset(buff,0,sizeof(buff));
f[0][0] = 1;
for(int i=1;i<=n;i++){
int ind = i%2;
int pre_ind = !ind;
int x = nums[i-1];
memset(buff[ind],0,sizeof(buff[ind]));
for(int j=-sum;j<=sum;j++){
f[ind][j+x]+=f[pre_ind][j];
f[ind][j-x]+=f[pre_ind][j];
}
sum+=x;
}
return f[n%2][target];
}
};
leetcode-322. 零钱兑换
class Solution {
public:
//dp[n]=p 表示拼凑n大小的面额最少所需要的的面额数
//也就是dp[n] = min(dp[n-Vi]+1) ,Vi表示第i种钱币的面额 ,但要确保dp(Vi)状态可达
int coinChange(vector<int>& coins, int amount) {
// vector<int>dp(amount+1);
// dp[0] = 0;
// //设定不可达的值为-1
// for(int i=1;i<=amount;i++)dp[i] = -1;
// for(int i=1;i<=amount;i++){
// for(auto x:coins){
// if(i<x)continue;//如果硬币所剩面额太小
// if(dp[i-x]==-1)continue;//如果前序列拼凑不出来
// if(dp[i] == -1 || dp[i]>dp[i-x]+1)dp[i]=dp[i-x]+1;//如果此时第一次拼凑这个面额或者还有更少的拼凑面额的方法
// }
// }
// return dp[amount];
//方法二:dp[i][j]使用前i种硬币,拼凑j元钱最少使用的多少个
/*
dp[i][j] = min(dp[i-1][j],dp[i][j-x]+1) 从没有使用第i中硬币和使用了第i种硬币这里1是1个硬币
*/
int dp[amount+1];
//因为需要找到最少的零钱个数开始将每个位置都设置为极大值(题目要求-1)
for(int i=0;i<=amount;i++) dp[i] = -1;
dp[0] = 0;//拼凑0元钱最少使用0个硬币
for(auto x:coins){//先遍历每一种硬币
for(int j=x;j<=amount;j++){//使用正向刷表法
if(dp[j-x] == -1)continue;
if(dp[j] == -1|| dp[j-x]+1<dp[j])dp[j] = dp[j-x]+1;
dp[j] = min(dp[j],dp[j-x]+1);
}
}
return dp[amount];
}
};
leetcode-518. 零钱兑换 II
class Solution {
public:
int change(int amount, vector<int>& coins) {
//f[i][j]前i种硬币拼凑j元钱的方法总数
//f[i][j] = f[i-1][j]+f[i][j-x]
int f[amount+1];
memset(f,0,sizeof(f));
f[0] = 1;
for(auto x:coins){
for(int j=x;j<=amount;j++){
f[j]+=f[j-x];//没加之前f[j]相当于f[i-1][j] 加过之后成了f[i][j]
}
}
return f[amount];
}
};
leetcode-377. 组合总和 Ⅳ
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
unsigned f[target+1];
memset(f,0,sizeof(f));
f[0] = 1;
for(int i=1;i<=target;i++){
for(auto x:nums){
if(x>i) continue;
f[i]+=f[i-x];
}
}
return f[target];
}
};
leetcode-382. 链表随机节点
方法一:将链表圈成一个圈,每次随机向前走几步获取随机节点值
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
/** @param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node. */
int n;
ListNode *head;
Solution(ListNode* head):n(0),head(head) {
ListNode *p = head,*q;
//将源链表圈成一个圈
while(p)q=p,p=p->next,n+=1;
//此时q指向链表的最后一个节点
q->next = head;
}
/** Returns a random node's value. */
int getRandom() {
int x = rand()%n;
while(x--)head = head->next;
return head->val;
}
};
leetcode-77. 组合
class Solution {
public:
//深搜
void dfs(int i,int n,int cnt,
int k,vector<int>&buff,
vector<vector<int>>&ret){
/*i:当前可以选择的第一个数字是什么,
n:最多选择的数字是什么
cnt:已经选择了几个,k:最多选择几个,buff:当前选择的集合,
ret:当前存储结果的数组
*/
if(k == cnt){
ret.push_back(buff);
return ;
}
if(n-i+1<k-cnt){
//可以选择的比还需要选多少个( 能选的数字比要选的数字都少)
return ;
}
//如果选了第i个数字
buff[cnt] = i;
dfs(i+1,n,cnt+1,k,buff,ret);
//如果没选第i个数字
dfs(i+1,n,cnt,k,buff,ret);
return ;
}
vector<vector<int>> combine(int n, int k) {
vector<int>buff(k);
vector<vector<int>>ret;
dfs(1,n,0,k,buff,ret);
return ret;
}
};
leetcode-234. 回文链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
//翻转链表
ListNode *reverse(ListNode *head){
ListNode ret,*p = head,*q;
while(p){
q = p->next;
p->next = ret.next;
ret.next=p;
p = q;
}
return ret.next;
}
bool isPalindrome(ListNode* head) {
if(head == nullptr) return false;
ListNode *p=head,*q=head;
while(q->next && q->next->next){
p = p->next;
q = q->next->next;
}
p->next = reverse(p->next);
q = p->next;
p = head;
//此时再让两个指针一起往后去
while(q){
if(p->val!=q->val) return false;
p=p->next;
q=q->next;
}
return true;
}
};