刷题笔记-DP

从集合角度分析DP -闫氏dp法
https://www.acwing.com

01背包问题

思路:

代码1:

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N  = 1010;
int dp[N][N];
int a[N][2];
int n,v;

int main()
{
    cin >> n >> v;
    for(int i = 1; i <= n;i++)
        for(int j = 0; j < 2;j++)
            cin >> a[i][j];
    
    for(int i = 1; i <= n;i++)     //第几个重物
        for(int j = 1;j <= v;j++){  //容量
            int tmp = j - a[i][0];
            if(tmp < 0)
                dp[i][j] = dp[i-1][j];
            else
                dp[i][j] = max(a[i][1] + dp[i-1][tmp] , dp[i-1][j] );
        }
    cout << dp[n][v] << endl;
    return 0;
}

代码2 - 空间优化

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N  = 1010;
int dp[N];
int n,m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n;i++)
    {
        int v,w;
        cin >> v >> w;
        for(int j = m; j >= v;j--)
            dp[j] = max(dp[j] , dp[j-v] + w);
    }
    cout << dp[m] << endl;
    

    return 0;
}

摘花生

Hello Kitty想摘点花生送给她喜欢的米老鼠。
她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。
Hello Kitty只能向东或向南走,不能向西或向北走。
问Hello Kitty最多能够摘到多少颗花生。

输入格式

第一行是一个整数T,代表一共有多少组数据。
接下来是T组数据。
每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。
每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M。

输出格式

对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。

数据范围

(1≤T≤100)
(1≤R,C≤100)
(0≤M≤1000)

思路:

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 105;
int a[N][N],dp[N][N];
int main(void)
{
    int t;
    cin >> t;
    while(t--)
    {
        int r,c;
        cin >> r >> c;
        for(int i = 1 ; i <= r ; i++ )
            for(int j = 1 ; j <= c ; j++ )
                scanf("%d",&a[i][j]);
        
        for(int i = 1;i <= r;i++)
            for(int j = 1;j <= c;j++)
            dp[i][j] = max(dp[i-1][j] + a[i][j], dp[i][j-1] + a[i][j]);
        
        cout << dp[r][c] << endl;
    }
    return 0;
}

最长上升子序列

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数N。
第二行包含N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

(1≤N≤1000)
(10^9≤数列中的数≤10^9)

思路:

代码:

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N],dp[N];

int main(void)
{
    int n,ans = 0;
    cin >> n;
    for(int i = 1;i <= n;i++)
        cin >> a[i];
    for(int i = 1; i <= n; i++)
    {
        //预处理,至少只含本身,则序列长度为1
        dp[i] = 1;
        for(int j = 1; j < i; j++)
            if(a[i] > a[j])
                dp[i] = max(dp[i], dp[j] + 1);
        ans = max(ans , dp[i]);
    }
                
    cout << ans << endl;
    
    return 0;
}

代码2 - 使用二分优化

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N],dp[N];

int main(void)
{
    int n;
    cin >> n;
    for(int i = 0;i < n;i++)
        cin >> a[i];
        
    int j = 0;dp[0] = a[0];
    for(int i = 1; i < n;i++)
    {
        int l = 0,r = j;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(a[i] <= dp[mid])
                r = mid;
            else
                l = mid + 1;
        }
        //特判
        if(r == j && a[i] > dp[j])
            dp[++j] = a[i];
        else
            dp[r] = a[i];
    }
    cout << j+1 << endl;
    return 0;
}

地宫取宝

X 国王有一个地宫宝库,是 n×m个格子的矩阵,每个格子放一件宝贝,每个宝贝贴着价值标签。
地宫的入口在左上角,出口在右下角。小明被带到地宫的入口,国王要求他只能向右或向下行走。
走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
当小明走到出口时,如果他手中的宝贝恰好是 k件,则这些宝贝就可以送给小明。
请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这k件宝贝。

输入格式

第一行3个整数,
n,m,k
接下来n行,每行有 m个整数 用来描述宝库矩阵每个格子的宝贝价值。

输出格式

输出一个整数,表示正好取 k个宝贝的行动方案数。该数字可能很大,输出它对1000000007取模的结果。

数据范围

(1≤n,m≤50)
(1≤k≤12)
(0≤宝贝的价值≤12)

思路:

1.数据范围很小,这说明For循环可能多重,dp数组的维度也可能有多维。
2.这一题是 前面 摘花生最长上升子序列两个问题的组合。

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 52, K = 14,MOD = 1000000007;
int w[N][N],f[N][N][K][K];
int n,m,k;
int main(void)
{
    cin >> n >> m >> k;
    for(int i = 1; i <= n;i++)
        for(int j = 1; j <= m;j++)
        {
            cin >> w[i][j];
            w[i][j]++;
        }
            
    //边界处理
    //w++表示如果拿了宝贝,一定存在价值,则w = 0 可以代表没有拿宝贝 
    //k = 0代表一个都不拿,也算一种方案数
    f[1][1][0][0] = 1;
    f[1][1][1][w[1][1]] = 1;
    //dp[i,j,num,c,val]表示走到坐标(i,j)拿了num个宝贝,且最大价值为val的总行动方案数
    //如果不取(i,j)处的宝贝,则dp[i,j,num,val] += dp[i-1,j,num,val] + dp[i,j-1,num,val]
    //如果取(i,j)处的宝贝,则dp[i,j,num,val] += dp[i-1,j,num-1,v'] + dp[i,j-1,num,val]  (w[i,j] == val, v' < val) 
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            for(int num = 0; num <= k; num++)
                for(int val = 0; val <= 13; val++) //因为不拿这个方案对应val == 0所以从0开始遍历
                {
                    //必须两个数就要模一次,否则可能3 * 10^9,可能溢出int
                    f[i][j][num][val] = ( f[i][j][num][val] + f[i-1][j][num][val]) % MOD;
                    f[i][j][num][val] = ( f[i][j][num][val] + f[i][j-1][num][val]) % MOD;
                    //num > 0表示 num = 1时从没有拿到拿一个的情况
                    if(w[i][j] == val && num > 0)
                        for(int v = 0; v < val; v++)
                        {
                            f[i][j][num][val] = ( f[i][j][num][val] + f[i-1][j][num-1][v] ) % MOD;
                            f[i][j][num][val] = ( f[i][j][num][val] + f[i][j-1][num-1][v] ) % MOD;
                        }
                }
    int ans = 0;
    for(int val = 1;val <= 13;val++)
        ans = ( f[n][m][k][val] + ans) % MOD;
    
    cout << ans << endl;
    
    return 0;
    
}

代码还是比较麻烦的,尤其需要注意边界的处理

波动数列

观察这个数列:
1 3 0 2 -1 1 -2 …
这个数列中后一项总是比前一项增加2或者减少3,且每一项都为整数。
栋栋对这种数列很好奇,他想知道长度为n和为s而且后一项总是比前一项增加a或者减少b的整数数列可能有多少种呢?

输入格式

共一行,包含四个整数n,s,a,b,含义如前面所述。

输出格式

共一行,包含一个整数,表示满足条件的方案数。
由于这个数很大,请输出方案数除以100000007的余数。

数据范围

(1≤n≤1000)
(-10^9<=s<=10^9)
(1<=a,b<=10^6)

思路:

1.推导

  • 假设符合条件的整数数列起始为x,后一项是前一项+a或-b,将这个操作统一为d_i = {a,-b};
  • 则数列为 (x,x+d_1,x+d_1+d_2,...,x+d_1+d_2+..+d_{n-1})
  • 则数列的和 (x + (x+d_1) + (x+d_1+d_2) + ... + (x+d_1+d_2+..+d_{n-1}) = S)
    • (nx + (n-1)d_1 + (n-2)d_2 +..+ d_{n-1} = S),且除了x,其余部分都是已知的
    • (x=S-[(n-1)d_1 + (n-2)d_2 +..+ d_{n-1}] / n)
  • 即对于每一序列D:(d_1,d_2,..,d_{n-1}),满足表达式F:((n-1)d_1 + (n-2)d_2 +..+ d_{n-1}) % n = 0,则惟一对应一个x
  • 为求合法整数序列的个数,需要求合法序列D的个数。

2.DP -背包模型

  • 因为每一个(d_i)都是等价的,所以F可以转化为((d_1 + 2d_2 +..+ (n-1)d_{n-1}) % n = 0
  • f[i , j]:i表示已经取了前i个d ,j表示F当前的余数。
  • 假设f[i ,j] = f[i-1, c],
  • 对于(d_i = a)有:i·a % n = j - c 则c = (j - i·a) % n; 对应的 c = (j + i·a) % n

代码:

#include <iostream>
using namespace std;
const int N = 1010, MOD = 100000007;
int f[N][N];
//求满足数学上定义的余数 - 余数一定为正数
int get_mod(int a,int b)
{
    return (a % b + b) % b;
}
int main(void)
{
    int n,s,a,b;
    cin >> n >> s >> a >> b;
    //边界处理:一项di都不取,则只有余数为0,才有一种方案
    f[0][0] = 1;
    for(int i = 1; i < n; i++)
        for(int j = 0; j < n;j++)
            f[i][j] = (f[i-1][get_mod(j + i * b, n)] + f[i-1][get_mod(j - i * a, n)]) % MOD;
    //这里S % n 要改成 get_mod(s,n),防止s是负数
    cout << f[n - 1][get_mod(s,n)] << endl;
    
    return 0;  
}
原文地址:https://www.cnblogs.com/zy200128/p/12631072.html