动态规划简单类型整理

Dynamic Programming

动态规划是一种穷举算法,通常基于一个递推公式和一个或多个初始状态。当前问题的解可以分解为多个子问题解得出。使用动态规划只需要多项式时间复杂度,因为比回溯法和暴力法快很多,体现了以空间换时间的算法思想

适用动态规划问题的特点:1.最优子结构,将母问题分解为子问题后,当子问题最优时,母问题通过优化选择一定最优的情况(或者说成母问题的最优解可由子问题的最优解构建得到)2.重复子序列,不同的决策序列,到达某个相同的阶段时,可能会产生相同的状态。3.无后效性,子问题的解一旦确定,就不再改变,不受它之后包含它的更大问题的求解决策影响

经典使用条件:dfs暴力计算无法满足程序特定时间对数据量的处理需求时,用动态规划以时间换空间

分类有:线性dp,区间dp,背包dp,树形dp,状态压缩dp,数位dp,计数型dp,递推型dp,概率型dp,博弈型dp,记忆化搜索

使用动态规划时,必须掌握的一个技巧是滚动数组优化,这种方法可以在不损失时间的情况下换取空间,方法是除去第一项并仔细斟酌顺序,注重动态规划状态的0/1态的分类判断;另外一个技巧是使用动态规划时候先列表格找规律;必须注意初始状态的定义

线性dp

单串

最长上升子序列(LIS,Longest Increasing Subsequence)
//O(n^2)的DP法
#include<bits/stdc++.h>
using namespace std;

int a[6000],dp[6000];

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		dp[i]=1;
		cin>>a[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<i;j++)
		{
			if(a[j]<a[i])
			{
				dp[i]=max(dp[i],dp[j]+1);
			}
		}
	}
	int maxn=0;
	for(int i=1;i<=n;i++)
	{
		//cout<<dp[i]<<endl;
		maxn=max(maxn,dp[i]);
	}
	cout<<maxn;
	return 0;
}
最长有效括号
#include<bits/stdc++.h>
using namespace std;

string s;

int main()
{
	cin>>s;
	int n=s.length();
  int dp[30010];
  for(int i=0;i<n;i++)
  {
  	dp[i]=0;
  }
  for(int i=0;i<n;i++)
  {
    if(s[i]=='(')
    {
      dp[i]=0;
    }
    else
    {
      if(dp[i-1]=='(')
      {
        dp[i]=dp[i-2]+2;
      }
      else
      {
        if(s[i-1-dp[i-1]]=='(')
        {
         dp[i]=dp[i-1]+2+dp[i-2-dp[i-1]];
        }
      }
    }
  }
  int maxn=0;
  for(int i=0;i<n;i++)
  {
    maxn=max(maxn,dp[i]);
  }
  cout<<maxn;
  return 0;
}
摆动序列

通过列表法,观察前几项数据的共同特点,从而做出结论,列表法数据表项尽量要列得细,不要害怕去枚举举例计算(实在不行可以尝试计算机暴力出前几个解,再找规律)

最大整除子集

基本思路与LIS一样,不同的是有先排序的操作,并且要输出它的最大路线之一(根据答案从后往前输,其实也可以建一个辅助数组,存储答案路径的前驱结点)

class Solution {
public:
    vector<int> largestDivisibleSubset(vector<int>& nums) {
        int len = nums.size();
        // 排序,目的使整除关系有序
        sort(nums.begin(), nums.end());
        vector<int> dp(len, 1);
        // 定义最大长度和最大值
        int maxLen = 1;
        int maxVal = dp[0];
        // 窗口从第二个开始,即包括1,2两个位置的元素开始
        for(int i = 1; i < len; i ++) {
            for(int j = 0 ; j < i; j ++) {
                // 更新dp[i]
                if(nums[i] % nums[j] == 0){
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            // 更新最大长度和最大值
            if(dp[i] > maxLen) {
                maxLen = dp[i];
                maxVal = nums[i];
            }
        }
        // 定义返回的数组
        vector<int> res;
        if(maxLen == 1) {
            res.push_back(nums[0]);
            return res;
        }
        // 倒序遍历
        for(int i = len - 1; i >= 0 && maxLen > 0; i --) {
            if(dp[i] == maxLen && maxVal % nums[i] == 0) {
                res.push_back(nums[i]);
                maxLen --;
                maxVal = nums[i];
            }
        }
        return res;
    }
};
分割数组的最大值

其实可以用二分来做,因为它特有的不减性质(可以相等的单调),用dp方法时状态转移方程式较难
d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , m a x ( d p [ k ] [ j − 1 ] , s u b [ i ] − s u b [ k ] ) ) dp[i][j] = min(dp[i][j], max(dp[k][j - 1], sub[i] - sub[k])) dp[i][j]=min(dp[i][j],max(dp[k][j1],sub[i]sub[k]))
思路是遍历k为倒数第一个数组的开始,使得由小到大的状态转移成立

乘积最大子数组
int maxProduct(vector<int>& nums) {
    int cur_max;
    vector<int> max_sub_arr(nums.size(),0),min_sub_arr(nums.size(),0);
    max_sub_arr[0]=nums[0];
    min_sub_arr[0]=nums[0];
    cur_max=max_sub_arr[0];
    for(int i=1;i<nums.size();i++)
    {
        max_sub_arr[i]=max(max_sub_arr[i-1]*nums[i],min_sub_arr[i-1]*nums[i]);
        max_sub_arr[i]=max(max_sub_arr[i],nums[i]);
        min_sub_arr[i]=min(max_sub_arr[i-1]*nums[i],min_sub_arr[i-1]*nums[i]);
        min_sub_arr[i]=min(min_sub_arr[i],nums[i]);
        cur_max=cur_max>max_sub_arr[i]?cur_max:max_sub_arr[i];
    }        
    return cur_max;             
}
俄罗斯套娃信封问题

和LIS的想法一模一样

双串

最长公共子序列 (LCS)

根本思路是递推,二维的动态规划(经典升维,dp做不了怎么办,那就再加一维)

#include<bits/stdc++.h>
using namespace std;

const int N=5e3;
int n,a[N],b[N],LCS[N][N];

int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<=n;i++)
	{
		cin>>b[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(a[i]==b[j]) LCS[i][j]=LCS[i-1][j-1]+1;
			else LCS[i][j]=max(LCS[i-1][j],LCS[i][j-1]);
		}
	}
	cout<<LCS[n][n];
	return 0;
} 
交叉字符串

把一二串的个数用二维标记,递推求解

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n1 = s1.size();
        int n2 = s2.size();
        if ( n1 + n2 != s3.size() ) return false;
        vector<vector<bool>> f( n1 + 1, vector<bool>(n2 + 1) );
        f[0][0] = true;
        for ( int i = 0; i <= n1; ++i ) {
            for (int j = 0; j <= n2; ++j ) {
                if ( i && s3[i + j - 1] == s1[i - 1] ) {
                    f[i][j] = f[i - 1][j];
                }
                if ( j && s3[i + j - 1] == s2[j - 1] )
                    f[i][j] = f[i][j] | f[i][j - 1];
            }
        }
        return f[n1][n2];
    }
};
不同的子序列

转移方程
$$
如果s[i-1] == t[j-1]:则dp[i][j] = dp[i-1][j-1] + dp[i][j-1]\

如果s[i-1] != t[j-1]:则dp[i][j] = dp[i][j-1]
$$
如果相同:相同的数量+不相同的数量;如果不相同:不相同的数量

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        dp =[[0] * (len(s)+1) for _ in range(len(t)+1)]
        for _ in range(len(s)+1):
            dp[0][_] = 1
        for i in range(1,len(t)+1):
            for j in range(1,len(s)+1):
                if s[j-1] == t[i-1]:
                    dp[i][j] = dp[i-1][j-1] + dp[i][j-1]
                else:
                    dp[i][j] = dp[i][j-1]

   return dp[i][j]
两个字符串的删除操作

最长公共子序列(LCS)的衍生,相同的思想

编辑距离

t e x t 1 [ i ] = t e x t 2 [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] t e x t 1 [ i ] ! = t e x t 2 [ j ] d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j − 1 ] ) text1[i]=text2[j]\quad dp[i][j]=dp[i−1][j−1]\\text1[i]!=text2[j]\quad dp[i][j]=min(dp[i−1][j],dp[i][j−1],dp[i−1][j−1]) text1[i]=text2[j]dp[i][j]=dp[i1][j1]text1[i]!=text2[j]dp[i][j]=min(dp[i1][j],dp[i][j1],dp[i1][j1])

通识符匹配

s [ i ] = = p [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = ? d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = ∗ d p [ i ] [ j ] = d p [ i ] [ j − 1 ] ∣ ∣ d p [ i − 1 ] [ j ] s[i]==p[j]\quad dp[i][j]=dp[i−1][j−1]\\p[j]==?\quad dp[i][j]=dp[i−1][j−1]\\p[j]==∗\quad dp[i][j]=dp[i][j−1]∣∣dp[i−1][j] s[i]==p[j]dp[i][j]=dp[i1][j1]p[j]==?dp[i][j]=dp[i1][j1]p[j]==dp[i][j]=dp[i][j1]∣∣dp[i1][j]

正则表达式匹配

s [ i ] = = p [ j ] d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = . d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] p [ j ] = = ∗ d p [ i ] [ j ] = d p [ i ] [ j − 1 ] p [ j ] = = ∗ & & ( s [ i ] = = p [ j − 1 ] ∣ ∣ p [ j − 1 ] = = . ) d p [ i ] [ j ] = d p [ i − 1 ] [ j ] s[i]==p[j]\quad dp[i][j]=dp[i−1][j−1]\\p[j]==.\quad dp[i][j]=dp[i−1][j−1]\\p[j]==∗\quad dp[i][j]=dp[i][j−1]\\p[j]==∗ \&\& (s[i]==p[j−1] ∣∣ p[j−1]==.)\quad dp[i][j]=dp[i−1][j] s[i]==p[j]dp[i][j]=dp[i1][j1]p[j]==.dp[i][j]=dp[i1][j1]p[j]==dp[i][j]=dp[i][j1]p[j]==&&(s[i]==p[j1]∣∣p[j1]==.)dp[i][j]=dp[i1][j]

经典问题

最大子序和

n u m s [ i ] = m a x ( n u m s [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) nums[i] = max(nums[i-1]+nums[i], nums[i]) nums[i]=max(nums[i1]+nums[i],nums[i])

int main()
{
	int sum = nums[0];
	int max_sum = nums[0];
	for(int i=1; i<numsSize; i++)
    {  
		if(sum > 0)            // 第 i 天是正数
			sum += nums[i];
		else                    // 如果是负数,那抛弃之前的,从当前天开始往后走
			sum = nums[i];
		max_sum = max(sum, max_sum); 
	}
	return max_sum;	
}
三角形的最小路径和

设 f(i, j) 为点 (i, j) 到底部的最小路径和,递推得状态转移方程
f ( i , j ) = m i n ( f ( i + 1 , j ) , f ( i + 1 , j + 1 ) ) + t r i a n g l e [ i ] [ j ] f(i, j) = min(f(i+1, j), f(i+1, j+1)) + triangle[i][j] f(i,j)=min(f(i+1,j),f(i+1,j+1))+triangle[i][j]

数字三角形

加了一维用k表示可以对k个数字进行乘运算,并且滚动数组
f [ j ] [ u ] = m a x ( f [ j ] [ u ] , f [ j − 1 ] [ u ] ) + a [ i ] [ j ] ; i f ( u ) f [ j ] [ u ] = m a x ( f [ j ] [ u ] , m a x ( f [ j ] [ u − 1 ] , f [ j − 1 ] [ u − 1 ] ) + P ∗ a [ i ] [ j ] ) ; f[j][u]=max(f[j][u],f[j-1][u])+a[i][j];\\if(u)f[j][u]=max(f[j][u],max(f[j][u-1],f[j-1][u-1])+P*a[i][j]); f[j][u]=max(f[j][u],f[j1][u])+a[i][j];if(u)f[j][u]=max(f[j][u],max(f[j][u1],f[j1][u1])+Pa[i][j]);

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define all(x) x.begin(),x.end()
#define foo(i,a,b) for(int i=a;i<=b;i++)
#define fro(i,a,b) for(int i=a;i>=b;i--)
#define int long long
const int N=5e5+10;
typedef pair<int,int> PII;
typedef long long LL;

const int P=6;

int n,k;
int a[110][110];
int f[110][11000];
signed main()
{
    //cout<<fixed<<setprecision(2);
    ios::sync_with_stdio(false); cin.tie(0);
    cin>>n>>k;
    k=min(k,n);
    memset(a,-63,sizeof a);
    fro(i,n,1)
    {
        foo(j,1,i)cin>>a[i][j];
    }
    memset(f,-63,sizeof f);
    f[1][0]=0;//注意此处类似滚动数组版本,用f[1][0]最合适,用f[0][0]时需要一些处理 
    int ans=-0x3f3f3f3f3f3f3f3f;
    //cout<<ans<<endl;
    foo(i,1,n)
    {
        fro(j,i,1)
        {
            fro(u,k,0)
            {
                if(u>i)continue;
                else
                {
                    f[j][u]=max(f[j][u],f[j-1][u])+a[i][j];
                    if(u)f[j][u]=max(f[j][u],max(f[j][u-1],f[j-1][u-1])+P*a[i][j]);
                }
            }
        }
    }
    foo(i,1,n)
    {
        foo(j,0,k)
        {
            ans=max(ans,f[i][j]);
        }
    }
    cout<<ans;
}
鸡蛋掉落

反向思考,递推的动态规划想法,可以先列表法实验,状态转移方程
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 + d p [ i − 1 ] [ j ] 总次数 = 失败的检验层数 + 该层 + 成功的检验层数 dp[i][j]=dp[i-1][j-1]+1+dp[i-1][j]\\总次数=失败的检验层数+该层+成功的检验层数 dp[i][j]=dp[i1][j1]+1+dp[i1][j]总次数=失败的检验层数+该层+成功的检验层数

class Solution {
public:
    int superEggDrop(int K, int N) {
        int remainTestCount = 1;//穷举移动次数(测试的次数)
        while (getConfirmFloors(remainTestCount, K) < N){
            ++remainTestCount;
        }
        return remainTestCount;
    }
    //在remainTestCount个测试机会(扔鸡蛋的机会 或者移动的次数),eggsCount个鸡蛋可以确定的楼层数量
    int getConfirmFloors(int remainTestCount, int eggsCount){
        if (remainTestCount == 1 || eggsCount == 1){(难点三、四)
            //如果remainTestCount == 1你只能移动一次,则你只能确定第一楼是否,也就是说鸡蛋只能放在第一楼,如果碎了,则F == 0,如果鸡蛋没碎,则F == 1
            //如果eggsCount == 1鸡蛋数为1,它碎了你就没有鸡蛋了,为了保险,你只能从第一楼开始逐渐往上测试,如果第一楼碎了(同上),第一楼没碎继续测第i楼,蛋式你不可能无限制的测试,因为你只能测试remainTestCount次
            return remainTestCount;
        }
        return getConfirmFloors(remainTestCount - 1, eggsCount - 1) + 1 + getConfirmFloors(remainTestCount - 1, eggsCount);
    }
};
打家劫舍I

d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] ) d p [ i ] [ 1 ] = d p [ i ] [ 0 ] + a [ i ] d p [ i ] [ 1 ] 为取, d p [ i ] [ 0 ] 为不取 dp[i][0]=max(dp[i-1][0],dp[i-1][1])\\dp[i][1]=dp[i][0]+a[i]\\dp[i][1]为取,dp[i][0]为不取 dp[i][0]=max(dp[i1][0],dp[i1][1])dp[i][1]=dp[i][0]+a[i]dp[i][1]为取,dp[i][0]为不取

int rob(int[] nums) {
    int n = nums.length;
    // 记录 dp[i+1] 和 dp[i+2]
    int dp_i_1 = 0, dp_i_2 = 0;
    // 记录 dp[i]
    int dp_i = 0; 
    for (int i = n - 1; i >= 0; i--) {
        dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
        dp_i_2 = dp_i_1;
        dp_i_1 = dp_i;
    }
    return dp_i;
}
打家劫舍II

环装的处理:给第一个做分类,要么偷,倒数第二个必须不偷;要么不偷

class Solution {
public:
    int rob(vector<int>& nums) {
       int n = nums.size();
       if(n == 1) return nums[0];       //只有一间房间,返回nums[0]
       vector<int>f(n + 1), g(n + 1);
       f[1] = nums[0], g[2] = nums[1];  //初始化
       for(int i = 2; i <= n - 1; i++)  f[i] = max(f[i - 1], f[i - 2] + nums[i - 1]); //区间[1,n-1]最大值
       for(int i = 3; i <= n; i++)      g[i] = max(g[i - 1], g[i - 2] + nums[i - 1]); //区间[2,n]最大值
       return max(f[n - 1], g[n]);
    }
};
买卖股票的最佳时机I

只能买卖一次,前后取差值,dp找最大的连续最大数列

或者
d p [ 0 ] [ j ] = m a x ( d p [ 0 ] [ j − 1 ] , d p [ 1 ] [ j − 1 ] + p r i c e s [ j ] ) d p [ 1 ] [ j ] = m a x ( d p [ 1 ] [ j − 1 ] , − p r i c e s [ j ] ) 其中 0 指不持股, 1 指持股 dp[0][j] = max(dp[0][j-1], dp[1][j-1] + prices[j])\\dp[1][j] = max(dp[1][j-1], -prices[j])\\其中0指不持股,1指持股 dp[0][j]=max(dp[0][j1],dp[1][j1]+prices[j])dp[1][j]=max(dp[1][j1],prices[j])其中0指不持股,1指持股

def maxProfit_opt(self, prices):
    size = len(prices)
    if size == 0 or size == 1:
        return 0
    dp1 = 0
    dp2 = -prices[0]
    for j in range(1, size)://空间优化
        tmp1 = max(dp1, dp2+prices[j])
        tmp2 = max(dp2, -prices[j])
        dp1, dp2 = tmp1, tmp2
    return dp1
买卖股票的最佳时机II

不限制购买,前后取差值,累加大于0的数字

或者:
d p [ 0 ] [ j ] = m a x ( d p [ 0 ] [ j − 1 ] , d p [ 1 ] [ j − 1 ] + p r i c e [ j ] ) ; d p [ 1 ] [ j ] = m a x ( d p [ 1 ] [ j − 1 ] , d p [ 0 ] [ j − 1 ] − p r i c e [ j ] ) 其中 0 指不持股, 1 指持股 dp[0][j]=max(dp[0][j-1],dp[1][j-1]+price[j]);\\dp[1][j]=max(dp[1][j-1],dp[0][j-1]-price[j])\\其中0指不持股,1指持股 dp[0][j]=max(dp[0][j1],dp[1][j1]+price[j]);dp[1][j]=max(dp[1][j1],dp[0][j1]price[j])其中0指不持股,1指持股

def maxProfit_opt(self, prices):

    size = len(prices)
    if size == 0 or size == 1:
        return 0
    # 初始化动态数组
    dp1 = 0
    dp2 = -prices[0]
    for j in range(1, size)://空间优化
        tmp1 = max(dp1, dp2 + prices[j])
        tmp2 = max(dp2, dp1 - prices[j])
        dp1, dp2 = tmp1, tmp2
    return dp1
买卖股票的最佳时机III

交易两次,求最大利润,加一维
d p [ i ] [ j ] = m a x { d p [ i ] [ j − 1 ] , m a x { p r i c e s [ i ] − p r i c e s [ n ] + d p [ i − 1 ] [ n ] } } , n = 0 , 1 , … , j − 1 dp[i][j]=max\left \{dp[i][j-1], max\left \{ prices[i]-prices[n]+dp[i-1][n] \right \} \right \} , n=0,1,…,j-1 dp[i][j]=max{dp[i][j1],max{prices[i]prices[n]+dp[i1][n]}},n=0,1,,j1

买卖股票的最佳时机Ⅳ

如上题
d p [ i ] [ j ] = m a x { d p [ i ] [ j − 1 ] , m a x { p r i c e s [ i ] − p r i c e s [ n ] + d p [ i − 1 ] [ n ] } } , n = 0 , 1 , … , j − 1 dp[i][j]=max\left \{dp[i][j-1], max\left \{ prices[i]-prices[n]+dp[i-1][n] \right \} \right \} , n=0,1,…,j-1 dp[i][j]=max{dp[i][j1],max{prices[i]prices[n]+dp[i1][n]}},n=0,1,,j1

def maxProfit(self, k, prices):

    size = len(prices)
    if size == 0:
        return 0
    dp = [[0 for _ in range(size)] for _ in range(k+1)]
    print(dp)

    for i in range(1, k+1):
        # 每一次交易的最大利润
        max_profit = -prices[0]
        for j in range(1, size):
            dp[i][j] = max(dp[i][j-1], max_profit + prices[j])
            max_profit = max(max_profit, dp[i-1][j] - prices[j])
    print(dp)
    return dp[-1][-1]
买卖股票的最佳时机含冷冻期

状态转移方程
d p [ 0 ] [ j ] = m a x ( d p [ 0 ] [ j − 1 ] , d p [ 2 ] [ j − 1 ] ) d p [ 1 ] [ j ] = m a x ( d p [ 0 ] [ j − 1 ] − p r i c e [ j ] , d p [ 1 ] [ j − 1 ] ) d p [ 2 ] [ j ] = d p [ 1 ] [ j − 1 ] + p r i c e [ j ] i = 0 表示未购状态, i = 1 表示持有状态, i = 2 表示冷却状态 dp[0][j]=max(dp[0][j-1],dp[2][j-1])\\dp[1][j]=max(dp[0][j-1]-price[j],dp[1][j-1])\\dp[2][j]=dp[1][j-1]+price[j]\\i=0表示未购状态,i=1表示持有状态,i=2表示冷却状态 dp[0][j]=max(dp[0][j1],dp[2][j1])dp[1][j]=max(dp[0][j1]price[j],dp[1][j1])dp[2][j]=dp[1][j1]+price[j]i=0表示未购状态,i=1表示持有状态,i=2表示冷却状态

买卖股票的最佳时机含手续费

d p [ 0 ] [ j ] = m a x ( d p [ 0 ] [ j − 1 ] , d p [ 1 ] [ j − 1 ] + p r i c e [ j ] − f e e ) ; d p [ 1 ] [ j ] = m a x ( d p [ 1 ] [ j − 1 ] , d p [ 0 ] [ j − 1 ] − p r i c e [ j ] ) 其中 0 指不持股, 1 指持股 dp[0][j]=max(dp[0][j-1],dp[1][j-1]+price[j]-fee);\\dp[1][j]=max(dp[1][j-1],dp[0][j-1]-price[j])\\其中0指不持股,1指持股 dp[0][j]=max(dp[0][j1],dp[1][j1]+price[j]fee);dp[1][j]=max(dp[1][j1],dp[0][j1]price[j])其中0指不持股,1指持股

区间dp

最长回文子串

正着反着都一样的一个区域就是回文区域,即找双串的最长相同串,时间复杂度O(n2)

最长回文子序列(LPS)

正着和反着的最长公共子序列

背包dp

是比较简单的一种dp类型,写之前先列一组数据算一算,列出方程和初始状态

01背包

背包有固定容量,判断取不取物体(物体只有一个)以达到最大的价值
v a l u e [ i ] [ j ] = m a x ( v a l u e [ i ] [ j ] , v a l u e [ i − 1 ] [ j − u s e d [ j ] ] + v a l [ j ] ) value[i][j]=max(value[i][j],value[i-1][j-used[j]]+val[j]) value[i][j]=max(value[i][j],value[i1][jused[j]]+val[j])

洛谷 P1048 [NOIP2005 普及组] 采药
#include<bits/stdc++.h>
using namespace std;
#define foo(i,a,b) for(int i=a;i<b;i++)

const int N=130;
int n,m,f[1005];
struct herb
{
	int time,value;
}h[N];

signed main()
{
	cin>>n>>m;
	foo(i,1,m+1)
	{
		cin>>h[i].time>>h[i].value;
	}
	foo(i,1,m+1)
	{
		for(int j=n;j>=1;j--)
		{
			if(j>=h[i].time) f[j]=max(f[j-h[i].time]+h[i].value,f[j]);//二维转一维要看数据怎么替换的
			else f[j]=f[j];
		}
	}
	cout<<f[n];
	return 0;
}

完全背包

背包有固定容量,判断取不取物体(物体有无限个)以达到最大的价值
f i r s t : v a l u e [ i ] [ j ] = m a x ( v a l u e [ i ] [ j ] , v a l u e [ i − 1 ] [ j − u s e d [ j ] ] + v a l [ j ] ) s e c o n d : v a l u e [ i ] [ j ] = m a x ( v a l u e [ i ] [ j ] , v a l u e [ i ] [ j − u s e d [ j ] ] + v a l [ j ] ) first:value[i][j]=max(value[i][j],value[i-1][j-used[j]]+val[j])\\ second:value[i][j]=max(value[i][j],value[i][j-used[j]]+val[j]) first:value[i][j]=max(value[i][j],value[i1][jused[j]]+val[j])second:value[i][j]=max(value[i][j],value[i][jused[j]]+val[j])

洛谷 P1616 疯狂的采药
#include<bits/stdc++.h>
using namespace std;
#define foo(i,a,b) for(int i=a;i<b;i++)

const signed N=1e7+10,M=1e4+10;
int t,m,f[N];
struct herb
{
	int time,value;
}h[M];

signed main()
{
	cin>>t>>m;
	foo(i,1,m+1)
	{
		cin>>h[i].time>>h[i].value;
	}
	foo(i,1,m+1)
	{
		foo(j,h[i].time,t+1)
		{
			f[j]=max(f[j],f[j-h[i].time]+h[i].value);//顺着来正好实现功能
		}
	}
	cout<<f[t]; 
	return 0;
}

多重背包

背包有固定容量,判断取不取物体(物体有有限个)以达到最大的价值,有两种思路,一种是二进制拆分、另一种是单调队列优化

二进制拆分就是把多重背包用完全二进制的思想拆成01背包

洛谷 P1776 宝物筛选
#include<bits/stdc++.h>
using namespace std;
#define foo(i,a,b) for(int i=a;i<b;i++)
#define fio(i,a,b) for(int i=a;i>b;i--)
const int N=1e5+10;

int n,W,f[N],v[N],w[N];

signed main()
{
	cin>>n>>W;
	int cnt,i=1,isq=1,mark,temp,add;
	while(i<=n)
	{
		cin>>v[isq]>>w[isq]>>cnt;
		temp=cnt;
		mark=1,add=0;
		while(temp>=mark)//二进制拆分
		{
			temp-=mark;
			v[isq+add]=v[isq]*mark;
			w[isq+add]=w[isq]*mark;
			mark*=2;
			add++;
		}
		if(temp!=0)
		{
			v[isq+add]=v[isq]*temp;
			w[isq+add]=w[isq]*temp;
			add++;
		}
		isq=isq+add;
		i++;
	}
	n=isq-1;
	foo(ii,1,n+1)//01背包
	{
		fio(j,W,w[ii]-1)
		{
			f[j]=max(f[j],f[j-w[ii]]+v[ii]);
		}
	}
	cout<<f[W];
	return 0;
}

单调队列优化(容易出错,不如直接二进制拆分)

洛谷 P1776 宝物筛选
#include<bits/stdc++.h>
#include<bits/stdc++.h>
using namespace std;

const int N=5e4;
int n,W,v,w,m;//价值、重量、个数
int dp[N],q1[N],q2[N],head,tail,ans;

int main()
{
	cin>>n>>W;
	for(int i=0;i<n;i++)
	{
		cin>>v>>w>>m;
		if(w==0)
		{
			ans+=v*m;
			continue;
		}
		int group=min(W/w,m);
		for(int d=0;d<w;d++)
		{
			head=0,tail=0;
			for(int j=0;j*w+d<=W;j++)
			{
				while(head<tail&&dp[d+j*w]-j*v>=q2[tail-1])
				{
					tail--;
				}
				q1[tail]=j;
				q2[tail]=dp[d+j*w]-j*v;
				tail++;
				while(head<tail&&q1[head]<j-group)
				{
					head++;
				}
				dp[d+j*w]=max(dp[d+j*w],q2[head]+j*v);
			}
		}
	}
	cout<<ans+dp[W];
	return 0;
}

二维费用背包

一样的思路,只是多加一个维度选择

P1855 榨取kkksc03
#include<bits/stdc++.h>
using namespace std;

int n,m,t,dp[210][210];

int main()
{
	cin>>n>>m>>t;
	int tempm,tempt;
	for(int i=0;i<n;i++)
	{
		cin>>tempm>>tempt;
		for(int mm=m;mm>=tempm;mm--)
		{
			for(int tt=t;tt>=tempt;tt--)
			{
				dp[mm][tt]=max(dp[mm][tt],dp[mm-tempm][tt-tempt]+1);
			}
		}
	}
	cout<<dp[m][t];
	return 0;
}

分组背包

把组作为最外循环,组中的元素作为最内侧循环,保证了只有一个的插入情况,注意理清楚每一组之间的数量

P1757 通天之分组背包
#include<bits/stdc++.h>
using namespace std;

int n,m,dp[1500]; 
struct things
{
	int weight;
	int value;
	int group;
}t[1500];

bool cmp(things a,things b)
{
	return a.group<b.group;
}

int main()
{
	cin>>m>>n;
	if(m==0)//特判m=0的情况
	{
		cout<<"0";
		exit(0);
	}
	for(int i=0;i<n;i++)
	{
		cin>>t[i].weight>>t[i].value>>t[i].group;
	}
	bool flag;
	sort(t,t+n,cmp);
	t[n].weight=20000;t[n].value=0;t[n].group=20000;
	for(int p=0;p<n;)
	{
		//cout<<"p=外层"<<p<<endl;
		int markp=p;
		for(int j=m;j>0;j--)
		{
			p=markp;
			flag=true;
			for(;flag||t[p].group==t[p+1].group;p++)
			{
				if(flag==false) break;
				//cout<<"flag=" <<flag<<endl;
				//cout<<"p="<<p<<endl;
				if(j>=t[p].weight) dp[j]=max(dp[j],dp[j-t[p].weight]+t[p].value);
				if(t[p].group!=t[p+1].group) flag=false;
			}
		}
	}
	cout<<dp[m];
	return 0;
}

有依赖的背包

一般思路:如果背包的附件比较少,可以枚举出来,可以转化为01背包类似的想法做,但是依赖的个数渐进是指数级的

P1064 [NOIP2006 提高组] 金明的预算方案
#include<bits/stdc++.h>
using namespace std;

const int maxn=4e4;

int n,m;
int v,p,q;
int main_item_w[maxn];
int main_item_c[maxn];
int annex_item_w[maxn][3];//这种分组的对应方式很清爽
int annex_item_c[maxn][3];
int f[maxn];

int main(){
  cin >> n >> m;
  for (int i=1;i<=m;i++){
    cin >> v >> p >> q;
    if(!q)
	{
        main_item_w[i] = v;
        main_item_c[i] = v * p;
    }
    else
    {
        annex_item_w[q][0]++;
	 	annex_item_w[q][annex_item_w[q][0]] = v;
        annex_item_c[q][annex_item_w[q][0]] = v * p;
    }
  }

  for (int i=1;i<=m;i++)
  {
    for (int j=n;main_item_w[i]!=0 && j>=main_item_w[i];j--){
          f[j] = max(f[j],f[j-main_item_w[i]]+main_item_c[i]);

          if (j >= main_item_w[i] + annex_item_w[i][1])
              f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] ] + main_item_c[i] + annex_item_c[i][1]);

          if (j >= main_item_w[i] + annex_item_w[i][2])
              f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][2]);

          if (j >= main_item_w[i] + annex_item_w[i][1] + annex_item_w[i][2])
              f[j] = max(f[j],f[ j - main_item_w[i] - annex_item_w[i][1] - annex_item_w[i][2] ] + main_item_c[i] + annex_item_c[i][1] + annex_item_c[i][2]);

       }
    }
   cout << f[n];
   return 0;
}

组合总和 Ⅳ

状态转移方程
d p [ i ] [ t ] = d p [ i − 1 ] [ t ] + d p [ i − 1 ] [ t − s [ i ] ] dp[i][t]=dp[i-1][t]+dp[i-1][t-s[i]] dp[i][t]=dp[i1][t]+dp[i1][ts[i]]

数位dp

本质上是一种用dp数组来记录的记忆化搜索

#include<bits/stdc++.h>
using namespace std;

int mem[15][15];

int dfs(string x,int pos,int past,bool zero,bool f)
{
	if(pos>=x.length())
	{
		return 1;
	}
	//cout<<"x= "<<x<<" "<<pos<<" "<<past<<" "<<zero<<" "<<f<<endl;
	if(zero!=0&&f!=0&&mem[pos][past]!=0)
	{
		//cout<<"I rem"<<"pos="<<pos<<" "<<past<<" "<<mem[pos][past]<<endl;
		return mem[pos][past];
	}
	int maxn=(f==false?(x[pos]-'0'):9);
	//cout<<"maxn= "<<maxn<<endl;
	int res=0;
	for(int i=0;i<=maxn;i++)
	{
		if(f==false)
		{
			if(zero==0)
			{
				res+=dfs(x,pos+1,i,i==0?0:1,f||i!=maxn);
			}
			else
			{
				if(abs(i-past)>=2)
				{
					res+=dfs(x,pos+1,i,zero,f||i!=maxn);
				}
			}
		}
		else
		{
			if(zero==0)
			{
				res+=dfs(x,pos+1,i,i==0?0:1,1);
			} 
			else if(abs(i-past)>=2)
			{
				res+=dfs(x,pos+1,i,1,1);
			}
		}
	}
	if(zero!=0&&f!=0)mem[pos][past]=res;
	return res;
}


int main()
{
//	for(int i=10000;i<=10222;i++)
//	{
//		cout<<i<<" "<<dfs(to_string(i),0,0,0,0)<<endl;
//		for(int j=0;j<15;j++)
//		{
//			for(int k=0;k<15;k++)
//			{
//				mem[j][k]=0;
//			}
//		}
//	}
	int a,b;
	cin>>a>>b;
	int t1=dfs(to_string(b),0,0,0,0);
	for(int j=0;j<15;j++)
	{
		for(int k=0;k<15;k++)
		{
			mem[j][k]=0;
		}
	}
	int t2=dfs(to_string(a-1),0,0,0,0);
	cout<<t1-t2;
	return 0;
}

概率dp

拿前面的概率做后面的状态转移

#include<bits/stdc++.h>
using namespace std;
#define int double

int dp[1010][1010];

signed main()
{
	signed a,b;
	cin>>a>>b;
	for(signed i=1;i<=a;i++)
	{
		dp[i][0]=1;
	}
	for(signed i=0;i<=b;i++)
	{
		dp[0][i]=0;
	}
	for(signed i=1;i<=a;i++)
	{
		for(signed j=1;j<=b;j++)
		{
			dp[i][j]=i*1.0/(i+j);
			if(i>=1&&j>=2)
			{
				dp[i][j]+=j*1.0*(j-1)*i/(i+j)/(i+j-1)/(i+j-2)*dp[i-1][j-2];
			}
			if(j>=3)
			{
				dp[i][j]+=j*1.0*(j-1)*(j-2)/(i+j)/(i+j-1)/(i+j-2)*dp[i][j-3];
			}
		}
	}
	printf("%.9lf",dp[a][b]);
	return 0;
}

求期望的时候要专注于状态转移,状态转移是动态规划的精髓,可能用到的数学知识很多,有复杂的数学计算,期望dp每一个空间存储的是达到的期望值

状态压缩dp

把一长串事情压缩成一个数字单独表示

#include<bits/stdc++.h>
using namespace std;

string a[20],b[20];
bool dp[70000][20],connect[20][20];
int n;

void dfs(int x,int y)
{
	dp[x][y]=1;
	//cout<<"B"<<x<<" "<<y<<endl;
	for(int i=0;i<n;i++)
	{
		//cout<<"pre"<<"i="<<i<<" "<<((1<<i)&x)<<" "<<connect[y][i]<<endl;
		if((!((1<<i)&x))&&connect[y][i])
		{
			//cout<<"C"<<endl;
			if(!dp[1<<i|x][i])
			dfs((1<<i)|x,i);
		}
	}
}

void solve()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>a[i]>>b[i];
	}
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			connect[i][j]=(a[i]==a[j]||b[i]==b[j]);
		}
	}
//	cout<<(1<<n)<<endl;
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=0;j<n;j++)
		{
			dp[i][j]=false;
		}
	}
	for(int i=0;i<n;i++)
	{
		dp[1<<i][i]=1;
		dfs(1<<i,i);
	}
	int ans=0;
	for(int i=0;i<(1<<n);i++)
	{
		for(int j=0;j<n;j++)
		{
			//cout<<"dp "<<i<<" "<<j<<" "<<__builtin_popcount(i)<<endl;
			if(dp[i][j]) ans=max(ans,__builtin_popcount(i));
		}
	}
	cout<<n-ans<<endl;
}

signed main(void)
{
	int T;
	cin>>T;
	while(T--)
	{
		solve();
	}
	return 0;
}
  • 13
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang八股文是指在面试或考试中常被问到的一些基础知识和常见问题。下面是一份简单整理: 1. Golang的特点:静态类型、垃圾回收、并发模型、内存安全、编译型语言等。 2. Golang中的基本数据类型:整型、浮点型、布尔型、字符串、数组、切片、映射、结构体等。 3. Golang中的控制流程:条件语句(if-else)、循环语句(for、range)、选择语句(switch)、跳转语句(break、continue)等。 4. Golang中的函数:函数定义、函数调用、参数传递(值传递和引用传递)、多返回值、匿名函数、闭包等。 5. Golang中的并发编程:goroutine的创建与调度、通道(channel)的使用、并发安全、锁机制(互斥锁、读写锁)等。 6. Golang中的错误处理:错误类型(error)、错误处理机制(defer、panic、recover)、错误码设计等。 7. Golang中的面向接口编程:接口的定义与实现、接口的多态性、空接口(interface{})、类型断言等。 8. Golang中的包管理:go mod的使用、依赖管理、版本管理等。 9. Golang中的测试与性能优化:单元测试(testing包)、性能剖析(pprof包)、内存分析、代码优化等。 10. Golang中的常用标准库:fmt、os、io、net、http、json等。 以上是一些常见的Golang八股文内容,希望对你有所帮助。当然,实际应用中还有很多其他方面的知识和技巧需要掌握。如果你有具体的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值