线性DP两题

好雨知时节,当春乃发生。
随风潜入夜,润物细无声。
野径云俱黑,江船火独明。
晓看红湿处,花重锦官城。——杜甫

题目一:数的划分

网址:https://www.luogu.com.cn/problem/P1025

题目描述

将整数n分成k份,且每份不能为空,任意两个方案不相同(不考虑顺序)。

例如:n=7,k=3,下面三种分法被认为是相同的。

1, 1, 5;
1, 5, 1;
5, 1, 1.

问有多少种不同的分法。

输入格式

n, k(6<n≤200,2≤k≤6)

输出格式

1个整数,即不同的分法。

输入输出样例
输入
7 3
输出
4
说明/提示

四种分法为:
1, 1, 5;
1, 2, 4;
1, 3, 3;
2, 2, 3.

方法一:搜索

搜索框架不难,可以加记忆化,可以优化和剪枝。

代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn = 200 + 5, maxk = 7;
int n, k, d[maxn][maxn][maxk];
int dfs(int p, int cur, int dep)
{
	if(p < 0 || dep < 0) return 0;//不符题意
	int &ans = d[p][cur][dep];
	if(cur * dep > p || cur > n) return ans = 0;//剪枝操作
	
	if(ans != -1) return ans;//记忆化
	if(!dep)
	{
		if(!p) return ans = 1;
		return ans = 0;
	}
	return ans = dfs(p, cur + 1, dep) + dfs(p - cur, cur, dep - 1);//两种选择 
}
int main()
{
	memset(d, -1, sizeof(d));//初始化,防止重复遍历同一个结点
	scanf("%d %d", &n, &k);
	printf("%d
", dfs(n, 1, k));
	return 0;
}
方法二:动态规划

不难想到令dp[i, j]表示剩余数字为i,划分j段序列之方案总和。显然有:dp[i, j] = sigma{dp[i - l, j - 1] | 0 < l <= i}。

留意到1, 2, 3和2, 3, 1以及2, 1, 3算一种相同的方案(组合),刚刚的转移方程似乎是无法避免这点的。通过调整顺序,我们可以将转移方程的l移到最外层,从而保证没有重复地计算。

代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn = 200 + 5, maxk = 10 + 5;
int n, k, dp[maxn][maxk] = {};
int main()
{
	scanf("%d %d", &n, &k);
	dp[0][0] = 1;
	for(int l = 1; l <= n; ++ l)
	{
		for(int i = l; i <= n; ++ i)
		{
			for(int j = 1; j <= k; ++ j)
			{
				dp[i][j] += dp[i - l][j - 1];
			}
		}
	}
	printf("%d
", dp[n][k]);
	return 0;
}

其实可以更好。

对于该状态,统计个数时我们分两类讨论:划分的数中至少有一个为1和没有一个为1的方案数总和(显然是可以包含所有情况的)。
当至少有一个为1时方案数为:dp[i - 1, j - 1],因为既然是确定了划分的一个数是1,那么把它减去不影响结果(方案中等价于没有它)。
当没有一个数为1时,显然方案数等于每一个划分出来的数-1(形象地认为:给定i个球装进j个盒子,要求:每个盒子不能为空,那么如果每个盒子都装有多个球,那么从任意一个盒子中取出1个球不影响结果)。
则必有:dp[i, j] = dp[i - 1, j - 1] + dp[i - j, j],

代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn = 200 + 10;
int n, k, dp[maxn][7];
int main()
{
	scanf("%d %d", &n, &k);
	memset(dp, 0, sizeof(dp));
	for(int i = 1; i <= k; ++ i) dp[i][i] = 1;
	for(int i = 1; i <= n; ++ i)
	{
		for(int j = 1; j <= min(i - 1, k); ++ j)
		{
			dp[i][j] = dp[i - 1][j - 1] + dp[i - j][j];
		}
	}
	printf("%d
", dp[n][k]);
	return 0;
}
题目二:放苹果

网址:http://poj.org/problem?id=1664

题目描述

把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分发(5,1,1和1,1,5是同一种方法)

输入格式

第一行是测试数据的数目t(0 <= t <= 20),以下每行均包括二个整数M和N,以空格分开。1<=M,N<=10

输出格式

对输入的每组数据M和N,用一行输出相应的K。

输入输出样例
输入
1
7 3
输出
8
输入
3
3 2
4 3
2 7
输出
2
4
2

看到了这道题的区别:允许空集出现了。

但是,n多枚举0。问题易解了!

代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#define SIZE 10 + 5
using namespace std;
int m, n, dp[SIZE][SIZE];
int main()
{
	int T;
	scanf("%d", &T);
	while(T --)
	{
		scanf("%d %d", &m, &n);
		memset(dp, 0, sizeof(dp));
		dp[0][0] = 1;
		for(int k = 0; k <= m; ++ k)
		{
			for(int i = k; i <= m; ++ i)
			{
				for(int j = 1; j <= n; ++ j)
				{
					dp[i][j] += dp[i - k][j - 1];
				}
			}
		}
		printf("%d
", dp[m][n]);
	}
	return 0;
}

其实可以仿照之前的做法。

有dp[i, j] = dp[i, j - 1] + dp[i - j, j]。留意j的范围是[1, n],因此,当i < j时,由抽屉原理可得:必有盘子是空的,因此,dp[i, j] = dp[i, i]。

代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int maxn = 10 + 5;
int m, n, dp[maxn][maxn];
int main()
{
	int T;
	scanf("%d", &T);
	while(T --)
	{
		scanf("%d %d", &m, &n);
		memset(dp, 0, sizeof(dp));
		dp[1][1] = 1;
		for(int i = 1; i <= n; ++ i) dp[0][i] = 1;
		for(int i = 1; i <= m; ++ i)
		{
			for(int j = 1; j <= n; ++ j)
			{
				if(i < j) dp[i][j] = dp[i][i];
				else dp[i][j] = dp[i][j - 1] + dp[i - j][j];
			}
		}
		printf("%d
", dp[m][n]);
	}
	return 0;
}
原文地址:https://www.cnblogs.com/zach20040914/p/12853262.html