dp 部分和问题及其扩展

Posted 桂月二四

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dp 部分和问题及其扩展相关的知识,希望对你有一定的参考价值。

**

一.部分和问题

**
给定整数 a1、a2、…、an,判断是否可以从中选出若干数,使它们的和恰好为 k。
首先,我们定义dp数组 dp[i][j] 代表使用前i个数字能否组成j这个数字
其次,我们发现dp[i][j] 是可以由dp[i-1][j-a[i]] 推出的,因此我们由状态转移方程 dp[i][j] |= dp[i-1][j-k* a[i] (k=0,1 且 j>=k*a[i[)
初始化:dp[0][0] = 0;

#include <bits/stdc++.h>
#define ll long long 
#define INF 0x3f3f3f3f
using namespace std;
int a[1001];
int dp[1001][1001]; 
int main()

	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	dp[0][0] = 1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=k;j++)// 注意要从0开始 
		
			dp[i][j]|=dp[i-1][j];
			if(j>=a[i]) dp[i][j]|=dp[i-1][j-a[i]];
//			为了和后面统一也可以写成如下的形式 
//			for(int k=0;k<=1&&k*a[i]<=j;k++)
//				dp[i][j]|=dp[i-1][j-k*a[i]];
		 
	if(dp[n][k]) cout<<"YES";
	else cout<<"NO";
    return 0;
  

**

2.多重部分和问题

**

事实上,我们只需要在部分和代码上稍加改变就可以变成多重部分和代码;
将上述代码的状态转移方程改为dp[i][j] |= dp[i-1][j-k* a[i] (k<=m[i] 且 j>=k*a[i])即可

#include <bits/stdc++.h>
#define ll long long 
#define INF 0x3f3f3f3f
using namespace std;
int a[1001];
int dp[1001][1001]; 
int m[1001];
int main()

	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	for(int i=1;i<=n;i++)
		cin>>m[i]; 
	dp[0][0] = 1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=k;j++)// 注意要从0开始 
		
			for(int k=0;k<=m[i]&&k*a[i]<=j;k++)
				dp[i][j]|=dp[i-1][j-k*a[i]];
		 
	cout<<dp[n][k];
    return 0;
  

**

三.变式,计算种类数

**
给定整数 a1、a2、…、an,每种数各有mi个,现在取出若干个数,使其等于k,由多少种取法?

这个问题也是一个简单的改编;
如果理解了上述的代码,很快就能发现将之前的状态转移方程的|= 改成+=即可 状态转移方程:dp[i][j] += dp[i-1][j-k* a[i] (k<=m[i] 且 j>=k*a[i])

代码请参考上一题

**

四. 在判断的基础上,输出相应的数字

**
问题 B: 新年趣事之打牌
题目描述
过年的时候,大人们最喜欢的活动,就是打牌了。xiaomengxian不会打牌,只好坐在一边看着。
这天,正当一群人打牌打得起劲的时候,突然有人喊道:“这副牌少了几张!”众人一数,果然是少了。于是这副牌的主人得意地说:“这是一幅特制的牌,我知道整副牌每一张的重量。只要我们称一下剩下的牌的总重量,就能知道少了哪些牌了。”大家都觉得这个办法不错,于是称出剩下的牌的总重量,开始计算少了哪些牌。由于数据量比较大,过了不久,大家都算得头晕了。
这时,xiaomengxian大声说:“你们看我的吧!”于是他拿出笔记本电脑,编出了一个程序,很快就把缺少的牌找了出来。
如果是你遇到了这样的情况呢?你能办到同样的事情吗?
输入
第一行一个整数TotalW,表示剩下的牌的总重量。
第二行一个整数N(1<N<=100),表示这副牌有多少张。
接下来N行,每行一个整数Wi(1<=Wi<=1000),表示每一张牌的重量。
输出
如果无解,则输出“0”;如果有多解,则输出“-1”;否则,按照升序输出丢失的牌的编号,相邻两个数之间用一个空格隔开。
样例输入
270
4
100
110
170
200
样例输出
2 4

分析发现,这就是一个部分和问题,但是题目要求输出丢失的牌子号。
我们可以引入path数组,path[j] = i,代表j这个数字可以由第i个数字(与其他数)相加形成,于是我们可以通过递归打印路径。但是事实上数据过大,递归可能炸掉,于是想到直接用栈。
(这里的代码将二维dp数组降为了一维数组)

#include <bits/stdc++.h>
#define ll long long 
using namespace std;
int a[105];
int dp[100005];
int path[100005];
int deta;
int tw,n;
void print(int j) 
    stack<int> s;
    while(j>0)
    
        s.push(path[j]);
        j-=a[path[j]];
    
    while(!s.empty())
    
        cout<<s.top()<<" ";
        s.pop(); 
     

int main()

    cin>>tw>>n;
    int sum = 0;
    for(int i=1;i<=n;i++)
    
        cin>>a[i];    
        sum+=a[i];
     
     deta = sum-tw;
    dp[0] = 1;
    for(int i=1;i<=n;i++)
    
        for(int j=deta;j>=a[i];j--)
        
                dp[j]+=dp[j-a[i]];  
                if(dp[j-a[i]]&&!path[j]) path[j] = i;//数字j可以由i组成 ,注意!path[j]这个条件 
        
    
    if(dp[deta]>=2) cout<<-1;
    else if (dp[deta]==0)  cout<<0;
    else print(deta);
    return 0;
  

**

五.例题

**
题目描述
珍珍学习乘法时,发现4=22,9=33,…,而2不可能分解为二个相同整数的乘积,但可以分解为11+11。她想知道对任意的正整数n,把它分解为几个整数与自身相乘之和,有多少种方案呢?
输入
只有一行,该行只有一个正整数n。
输出
只有一行,该行只有一个正整数,表示总方案数。
样例输入
4
样例输出
2
提示
30%的数据,1≤n≤10;

80%的数据,1≤n≤300;

100%的数据,1≤n≤800。

很简单吧!

#include <bits/stdc++.h>
#define ll long long 
#define INF 0x3f3f3f3f
using namespace std;
int a[34];
int dp[1001][1001]; 
int main()

    int n;cin>>n;
    for(int i=1;i<=33;i++)
        a[i] = i*i;
    dp[0][0] = 1;
    for(int i=1;i<=33;i++)
        for(int j=0;j<=n;j++)
        
            for(int k=0;j>=k*a[i];k++)
            dp[i][j]+=dp[i-1][j-k*a[i]];
        
    //for(int i=1;i<=n;i++) cout<<dp[i]<<" "; 
    cout<<dp[33][n];
    return 0;
  

以上是关于dp 部分和问题及其扩展的主要内容,如果未能解决你的问题,请参考以下文章

多重背包(dp专题)

总重量。删除锚部分网址

XTU 1233 Coins(DP)

BZOJ 1925 地精部落(DP)

LeetCode1049. 最后一块石头的重量 II / 牛客:毕业旅行问题(状压DP)

RQNOJ 622 最小重量机器设计问题:dp