挑战程序设计竞赛(第二章:2.3 动态规划)

01背包

相关知识链接acm之旅–动态规划(DP)中的0-1背包问题
复杂度O(nW)。n为种类数目,W为背包容量。

01背包2

当W太大,且v比较小时。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码:
在这里插入图片描述在这里插入图片描述

完全背包问题

参考博文:acm之旅–动态规划(DP)背包问题

最长公共子序列

参考博文:acm之旅–动态规划(DP)最长公共子序列。

最长上升子序列

参考博文:acm之旅–动态规划(DP)最长递增子序列。

多重部分和问题

在这里插入图片描述

  • 思路:一类典型的动态规划。难点在与状态的构造,不同的状态最终双方复杂度也不一样。
    • 方法一:
      • 状态的构造:d[i][j]表示使用前i种数字是否能加和成j(bool类型)。
      • 终态:d[n][K]。
      • 状态转移方程:用前i种数字加和成j等价于,只要用前i-1种数字加和成j、j-ai、j-mixai等中的一种就成立。
        d[i][j] = d[i][j] | d[i-1][j-kxai](0<=k<=mi且kxai<=j时)
      • 初始化:d[][] = false; d[0][0] = true;
      • 复杂度:O(nK ∑ 1 n m i \sum_1^nm_i 1nmi
      • DP存取bool结果浪费了许多信心。
    • 方法二:
      • 状态的构造:d[i][j]表示用前i种数加和得到j时第i种数最多能剩余多少个(不能加和得到i的情况下为-1)
      • 判断d[n][K]>=0
      • 状态转移方程:如果前i-1种数可以得到j时,第i个数就可以留下 m i m_i mi个。如果前i种数加和出 j- a i a_i ai时第i种数还剩下k(k>0)个,那么d[i][j] = k-1。如果k<0则表示该数字无法达到。
        d [ i ] [ j ] = { m i , d [ i − 1 ] [ j ] &gt; = 0 − 1 , j &lt; a i 或 者 d [ i ] [ j − a i ] &lt; = 0 d [ i ] [ j − a i ] − 1 , e l s e d[i][j] = \begin{cases} m_i &amp; &amp; ,d[i-1][j]&gt;=0 \\ -1 &amp; &amp; ,j&lt;a_i 或者 d[i][j-a_i]&lt;=0 \\ d[i][j-a_i]-1 &amp; &amp; ,else \end{cases} d[i][j]=mi1d[i][jai]1,d[i1][j]>=0,j<aid[i][jai]<=0,else
      • 初始化:dp[][] = -1;d[0][0] = 0,
      • 复杂度:O(nK)
      • 可以将二维数组一维化。

代码:
方法一:

#include <iostream>
#include <cstring>
using namespace std;

const int MAX = 10000;
int n, a[MAX], m[MAX], K;
int dp[MAX][MAX];

void Init()
{
    memset(dp, 0, sizeof(dp));
    dp[0][0] = 1;
}

void solve1()
{
    for(int i=1; i<=n; i++)
    {
        for(int j=0; j<=K; j++)
        {
            for(int k=0; k<=m[i] && k*a[i]<=j; k++)
            {
                dp[i][j] = dp[i][j] | dp[i-1][j-k*a[i]];
            }
        }
    }
    if(dp[n][K]) cout << "Yes" << endl;
    else         cout << "No" << endl;
}

int main()
{
    while(cin >> n)
    {
        for(int i=1; i<=n; i++)
            cin >> a[i];
        for(int i=1; i<=n; i++)
            cin >> m[i];
        cin >> K;

        Init();

        solve1();
    }
    return 0;
}

方法二:

int dp[MAX];
void solve()
{
	memset(dp, -1, sizeof(dp));
	dp[0] = 0;
	for(int i=0; i<n; i++)
	{
		for(int j=0; j<=K; j++)
		{
			if(dp[j]>=0)
			{
				dp[j] = m[i];
			}else if(j<a[i] || dp[j-a[i]]<=0)
			{
				dp[j] = -1;
			}else
			{
				dp[j] = dp[j-a[i]] - 1;
			}
		}
	}
	if(dp[K]>=0) printf("Yes\n");
	else         printf("No\n");
划分数

参考博文:挑战程序设计竞赛:划分数滚动数组

  • 题目大意:将数字n划分为m份数字,使得m个数字之和等于n,求划分的方法数。
  • 思路:一类典型的动态规划题。难点在于状态转移方程的确定,注意可以使得划分的数字为0。
    • 状态构造:dp[i][j]表示j的i划分的方法总数。
    • 终态:dp[m][n],表示要求得的n的m划分数。
    • 状态转移方程:使用两个条件来递推。1)当所有划分的数字均大于0时,n的m划分方法数实际上是等于n-m的m划分,即将n的m划分的数字均减去一。2)当存在划分的数字为0的情况时,n的m划分实际上等于n的m-1划分。
      d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] j &lt; i d p [ i − 1 ] [ j ] + d p [ i ] [ j − i ] j &gt; = i dp[i][j] = \begin{cases} dp[i-1][j] &amp; &amp; j&lt;i \\ dp[i-1][j]+dp[i][j-i] &amp; &amp; j&gt;=i \end{cases} dp[i][j]={dp[i1][j]dp[i1][j]+dp[i][ji]j<ij>=i
    • 初始化:dp[0][*] = 0, dp[0][0] = 1;

代码:

#include <iostream>
#include <cstring>
using namespace std;

const int MAX = 1001;
int m, n, M;
int dp[MAX][MAX];

int main()
{
   while(cin >> n >> m)
   {
       cin >> M;
       memset(dp, 0, sizeof(dp));
       dp[0][0] = 1;
       for(int i=1; i<=m; i++)
       {
           for(int j=0; j<=n; j++)
           {
               if(j>=i)
                   dp[i][j] = (dp[i-1][j]+dp[i][j-i])%M;
               else
                   dp[i][j] = dp[i-1][j];
           }
       }
       cout << dp[m][n] << endl;
   }
   return 0;
}
多重集组合数

参考博文:挑战程序设计竞赛:多重组合数

  • 题目大意:n种物品,第i种物品有ai个。从这些物品中取出m个,求其取法个数。
  • 思路:动态规划问题。重点在与复杂度的降低,两种思考。
    • 状态构造:dp[i][j]表示在前i中物品中可以取出j个的取法个数。
    • 终态:dp[n][m]。
    • 复杂度高(O(n*m^2)):dp[i][j]等价于在第i种中取出[0,min(ai,j)]个物品,其余的从前i-1中取。
    • 复杂度低(O(n*m)):dp[i][j]等价于两种情况:1)从第i种不取物品(dp[i-1][j])。2)从第i种中取至少1个物品。即dp[i][j-1]-dp[i-1][j-a[i]-1]。具体情况参考博文。
    • 初始化:dp[*][0] = 1。一个都不取的方法为1种。

代码:
类似题目:Ant Counting

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

typedef long long LL;
const int MAX = 100001;
const int MOD = 1000000;

int dp[1001][MAX];
int a[MAX];
int T, A, S, B;

void Init()
{
    for(int i=0; i<=1001; i++)
        dp[i][0] = 1;
}

void solve(int n)
{
    for(int i=1; i<=T; i++)
    {
        for(int j=0; j<=n; j++)
        {
            if(j-1-a[i]>=0)//取模时要避免减法运算结果出现负数
                dp[i][j] = (dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1-a[i]]+MOD)%MOD;
            else
                dp[i][j] = (dp[i-1][j]+dp[i][j-1])%MOD;
        }
    }
}
int main()
{
    int t;
    while(scanf("%d%d%d%d", &T, &A, &S, &B)!=EOF)
    {
        memset(a, 0, sizeof(a));
        for(int i=0; i<A; i++)
        {
            scanf("%d", &t);
            a[t]++;
        }

        Init();

        LL cnt = 0;
        solve(B);
        for(int i=S; i<=B; i++)
            cnt = (cnt+dp[T][i])%MOD;
        printf("%d\n", cnt);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值