关于动态规划
1、如何确定本问题可以使用动态规划
是否有后效性,对于某个问题,如果我们只关注某个状态,只关注状态的值,而不关注该状态是如何转移过来的,那么就是一个无后效性的问题,此时可以使用DP解决。
另外,可以根据数据范围来猜测是否使用DP解决,如果数据的范围在 1 0 5 − 1 0 6 10^{5} - 10^{6} 105−106,可以考虑使用一维DP来解决,如果数据范围在 1 0 2 − 1 0 3 10^{2} - 10^{3} 102−103,可以考虑使用二维DP来解决。
2、我们如何确定本题的状态定义
大部分DP的状态定义是根据经验去猜测的,但是一般状态定义是与结尾状态与答案有关的
3、我们如何确定状态转移方程
一般情况,状态转移方程就是对最后一步的分情况讨论,如果猜测了一个状态定义,但是无论如何也列不出涵盖所有情况的状态转移方程,那么多半是状 态定义错了.
4、对于状态转移的要求是什么
状态转移要不漏不重
如果是求最值,不漏即可
如果求方案数,要不重不漏
5、如何分析动态规划的时间复杂度
一般一维是O(N),二维是O(N^2)
01背包:有N件物品和重量为weight的背包,第i件物品消耗的重量为weights[i],获得的价值是values[i],放入哪些物品获得总价值最大?(每个物品只有一个)
解:状态定义:设f(i,w)为前i个物品最多消耗重量w所获取的价值
状态转移方程: f ( i , w ) = m a x ( f ( i − 1 , w ) , f ( i − 1 , w − w e i g h t s [ i ] ) + v a l u e s [ i ] ) f(i,w)=max(f(i-1,w),f(i-1,w-weights[i])+values[i]) f(i,w)=max(f(i−1,w),f(i−1,w−weights[i])+values[i])
- 递归方法
public static int dp(int[] weights,int[] values,int weight,int i){
if(i==-1){
return 0;
}
int max=dp(weights, values, weight, i-1);
if(weight>=weights[i]){
max=Math.max(values[i]+dp(weights, values, weight-weights[i], i-1),max);
}
return max;
}
- 非递归方法
public static int dp(int[] weights,int[] values,int weight){
int[][] dp=new int[weight+1][weights.length+1];
for(int i=1;i<=weight;i++){
for(int j=1;j<=weights.length;j++){
if(i>=weights[j-1]){
dp[i][j]=Math.max(dp[i-weights[j-1]][j-1]+values[j-1],dp[i][j-1]);
}else{
for(int k=0;k<j;k++){//这里要选择在消耗同重量的情况下,前多少个物品的价值之和最大 dp[i][j]=Math.max(dp[i][j],dp[i][k]);
}
}
}
}
return dp[weight][weights.length];
}
完全背包问题:有N件物品和重量为weight的背包,第i件物品消耗的重量为weights[i],获得的价值是values[i],放入哪些物品获得总价值最大?(每个物品无限多)
解:状态定义:f(i,w)表示使用完前i个物品消耗重量w所获得的最大价值
状态转移: f ( i , w ) = m a x ( f ( i − 1 , w ) , f ( i − 1 , w − x × w e i g h t s [ i ] + x × v a l u e s [ i ] ) , 其 中 x = [ 0 , w / w e i g h t s [ i ] ] f(i,w)=max(f(i-1,w),f(i-1,w-x×weights[i]+x×values[i]),其中x=[0,w/weights[i]] f(i,w)=max(f(i−1,w),f(i−1,w−x×weights[i]+x×values[i]),其中x=[0,w/weights[i]]
- 递归方法
public static int dp(int[] weights,int[] values,int weight,int i){
if(i==-1||weight<=0) return 0;
int x=weight/weights[i];
int max=0;
for(int j=1;j<=x;j++){
max=Math.max(max, dp(weights, values, weight-j*weights[i], i-1)+values[i]*j);
}
return Math.max(max,dp(weights, values, weight, i-1));
}
多重背包:有N种物品和重量为weight的背包,第i种物品的数量为nums(i),消耗的重量是weights[i],获得的价值是values[i],求放入哪些物品使得获得的总价值最大。
解:状态定义:f(i,w)表示使用完前i种物品,最多消耗w重量所获取的最大价值。
状态转移: f ( i , w ) = m a x ( f ( i − 1 , w ) , f ( i − 1 , w − x ∗ w e i g h t s [ i ] ) + x ∗ v a l u e s [ i ] ) , 其 中 x = [ 0 , m i n ( w / w e i g h t s [ i ] , n u m s [ i ] ) ] f(i,w)=max(f(i-1,w),f(i-1,w-x*weights[i])+x*values[i]),其中x=[0,min(w/weights[i],nums[i])] f(i,w)=max(f(i−1,w),f(i−1,w−x∗weights[i])+x∗values[i]),其中x=[0,min(w/weights[i],nums[i])]或者可以转化为01背包问题,比如有其中有2件容积为3,价值为4的物品a,我们可以转化为有两件物品,每个物容积和价值都是3和4.
- 递归方法
public static int dp(int[] weights,int[] values, int[] nums, int weight, int i){
if(i==-1||weight<=0)return 0;
int x=Math.min(weight/weights[i],nums[i]);
int max=0;
for(int j=0;j<=x;j++){
max=Math.max(max,dp(weights, values, nums, weight-weights[i]*j, i-1)+j*values[i]);
}
return max;
}
混合背包问题:混合背包问题:有N种物品和容量为weight的背包,第i种物品消耗的容量为weights[i],获得的价值为value[i],所有物品中,有的物品只能放入一件,有的物品可以放入有限件,还有物品可以放入无限件,求放入哪些物品获取的总价值最大?
解:状态定义:f(i,w)表示放入前i种物品,消耗最大容量为w时所获取的最大价值。
状态转移: f ( i , w ) = m a x ( f ( i − 1 , , w ) , f ( i − 1 , w e i g h t − w e i g h t s [ i ] ∗ x ) + x ∗ v a l u e s [ i ] ) , 其 中 x = [ 0 , m i n ( w / w e i g h t s [ i ] , n u m s [ i ] ) ] f(i,w)=max(f(i-1,,w),f(i-1,weight-weights[i]*x)+x*values[i]),其中x=[0,min(w/weights[i],nums[i])] f(i,w)=max(f(i−1,,w),f(i−1,weight−weights[i]∗x)+x∗values[i]),其中x=[0,min(w/weights[i],nums[i])]nums[i]=-1表示第i种物品可以放无限件,实际上和多重背包问题一样的。
- 递归方法
public static int dp(int[] weights,int[] values, int[] nums, int weight, int i){
if(i==-1||weight<=0)return 0;
if(nums[i]==-1)nums[i]=Integer.MAX_VALUE;
int x=Math.min(weight/weights[i],nums[i]);
int max=0;
for(int j=0;j<=x;j++){
max=Math.max(max,dp(weights, values, nums, weight-weights[i]*j, i-1)+j*values[i]);
}
return max;
}
二维01背包:有N种物品和载荷为weight,容量为volume的背包,第i种物品的消耗的载荷为weights[i]消耗的容量为volumes[i],获取的价值为values[i],求放入哪些物品获取的总价值最大。
解:状态定义:f(i,w,v)表示放入前i种物品,消耗载荷w和容量v所获取的最大容量。
状态转移: f ( i , w , v ) = m a x ( f ( i − 1 , w , v ) , f ( i − 1 , w − w e i g h t s [ i ] , v − v o l u m e s [ i ] ) + v a l u e s [ i ] ) f(i,w,v)=max(f(i-1,w,v),f(i-1,w-weights[i],v-volumes[i])+values[i]) f(i,w,v)=max(f(i−1,w,v),f(i−1,w−weights[i],v−volumes[i])+values[i])
- 递归方法
public static int dp(int[] weights,int[] volumes,int[] values,int weight,int volume,int i){
if(i==-1) return 0;
int max=dp(weights, volumes, values, weight, volume, i-1);
if(weight>=weights[i]&&volume>=volumes[i]){
max=Math.max(max, dp(weights, volumes, values, weight-weights[i], volume-volumes[i], i-1)+values[i]);
}
return max;
}
前缀和1:有N个正整数放到数组A里,现在要求新的数组B中第i个元素是A数组前i个元素之和
解:
public static void prefixSum1(int[] A,int[] B){
B[0]=A[0];
for(int i=1;i<A.length;i++){
B[i]=A[i]+B[i-1];
}
}
连续子数组1:求一个数组的连续子数组的总个数?连续指的是索引连续,比如[1,2,4],连续的子数组有[1],[2],[4], [1,2], [2,4], [1,2,4] 六个
- **解:**可以分别求取以每个元素为结尾的子数组数量,然后加起来即可。显然总数=1+2+…+nums.length.
连续子数组2:求一个数组的连续子数组的总个数,其中子数组中相邻元素的差为1?连续指的是索引连续,比如[1,2,4],连续的子数组有[1],[2],[4],[1,2],[2,4], [1,2,4]六个
**解:**这道题的子数组中的限制条件有两个,一个是索引相差1,另一个是相邻元素值相差1.
public static int subArray1(int[] nums){
int result=0;
int temp=0;
for(int i=1;i<nums.length;i++){
if(nums[i]-nums[i-1]==1){
temp++;
result+=temp;
}else{
temp=0;
}
}
return result;
}
唯一字串数目:把字符串 s 看作是“abcdefghijklmnopqrstuvwxyz”的无限环绕字符串,所以 s看起来是这样的:"…zabcdefghijklmnopqrstuvwxyzabcdefghijk lmnopqrstuvwxyzabc"。 现在我们有了另一个字符串 p 。你需要的是找出 s 中有多少个唯一的 p 的非空子 串,尤其是当你的输入是字符串 p,你需要输出字符串 s 中 p 的不同的非空子串的数目。
解:于当以某个字符结尾的字符串长度比另一个以该字符结尾的字符串长度长时,后面的字符串所形成的满足题目要求的子字符串肯定包含于前者形成的子字 符串集合。所以只需要找出以每个字符结束的最长子串的长度,然后加和所有的长度。
public static int substrNums(String p){
int result=0;
int temp=0;
int[] dp=new int[26];
char[] chars=p.toCharArray();
for(int i=0;i<chars.length;i++){
if(i>0&&(chars[i]-chars[i-1]-1)%26==0){
temp++;
}else{
temp=1;
}
int index=chars[i]-'a';
dp[index]=Math.max(dp[index], temp);
}
for(int i:dp)result+=i;
return result;
}
最长连续子数组:从数组中找到连续子数组数目,连续子数组需要满足其中的不同数字的个数不得超过K个。数组元素值小于等于20000,大于等于0.
**解:**使用滑动窗口,即快指针与满指针遍历数组,并且利用前缀和思想,如果窗口中不同数字个数达到K+1个时,就应该缩小窗口,也就是让慢指针前进,从 而使得窗口中的不同数字个数之和达到K个。
public static int subArray2(int[] nums,int K){
int result=0;
int[] dp=new int[20001];
int size=0;
int slow=0;
int fast=0;
while(fast<nums.length){
if(dp[nums[fast]]==0&&size==K){//此时说明如果加上本次遍历的元素,那么窗口中就有K+1个不同数字了,需要缩小 窗口至K个。
while(slow<fast){
dp[nums[slow]]--;
if(dp[nums[slow]]==0){//此时说明找到了第一个整数,这个整数原来在窗口中,现在已经刚好没有了,应该停止缩小窗口。
size--;
break;
}
slow++;
}
slow++;//slow加1是因为停止缩小窗口时,慢指针所在的位置是整数消失的位置,即应该将慢指针前进一个位置。
}
if(dp[nums[fast]]==0){
dp[nums[fast]]++;
size++;
}else{
dp[nums[fast]]++;
}
fast++;
result+=(fast-slow);//使用前缀思想,此时会新增加fast-slow个满足条件的连续子数组
}
return result;
}