DP
成绩 | 10 | 开启时间 | 2020年03月10日 星期二 07:55 |
折扣 | 0.8 | 折扣时间 | 2020年04月7日 星期二 23:55 |
允许迟交 | 否 | 关闭时间 | 2020年04月7日 星期二 23:55 |
对于由从1到N (1 <= N <= 39)这N个连续的整数组成的集合来说,我们有时可以将集合分成两个部分和相同的子集合。
例如,N=3时,可以将集合{1, 2, 3} 分为{1,2}和{3}。此时称有一种方式(即与顺序无关)。
N=7时,共有四种方式可以将集合{1, 2, 3, ..., 7} 分为两个部分和相同的子集合:
{1,6,7} 和 {2,3,4,5}
{2,5,7} 和 {1,3,4,6}
{3,4,7} 和 {1,2,5,6}
{1,2,4,7} 和 {3,5,6}
输入:程序从标准输入读入数据,只有一组测试用例。如上所述的N。
输出:方式数。若不存在这样的拆分,则输出0。
测试输入 | 期待的输出 | 时间限制 | 内存限制 | 额外进程 | |
---|---|---|---|---|---|
测试用例 1 |
|
| 1秒 | 64M | 0 |
我们将集合的元素和的一半记为sum,此题可以转化为求:集合{1..n}中和为 sum 的子集个数的一半。若sum不为整数,显然此题不存在这样的拆分。下面继续讨论存在解的情况:
定义一个 calc(n, sum) 函数:求集合{1..n}中和为sum的子集的个数。(为了防止反复递归、提高效率,将此函数的返回结果保存于数组dp[n][sum]中)
🔺至于函数如何计算,我们可以分两种情况思考:
- 当sum > n:那么其子集中不可含n,dp[n][sum] = calc(n-1, sum)(选择不含n)
- 当sun <= n:那么其子集中可以含n也可以不含n,dp[n][sum] = calc(n-1, sum) (选择不含n)+ calc(n-1, sum-n)(选择含n)
(其实每一层的 calc(n, sum) 函数就是在做选择:选 n或不选 n。很像背包问题:从1..n中选出若干物品放入背包使得背包恰好装满,此时是否将 n 放入背包呢 ?)
🔺递归的终点
1、当递归到 n = 0 时:
- 若sum = 0,说明此路选择恰好将 "背包填满"(选择出的子集和恰为最初的sum),算一个合理的结果,应该返回1
- 若sum > 0,说明此路选择 "背包没有满" (选出的子集和不够最初的sum),结果不合理,应返回0
2、当此次调用的calc(n,sum)已经被调用过:结果值已经存储在dp[n][sum]中,直接返回dp[n][sum]
注意:dp数组的初始化问题。
完整AC代码:
//
// Created by LittleCat on 2020/3/19.
//
#include <cstdio>
#include <cstring>
#define maxX 40
#define maxY 391
int dp[maxX][maxY];
/* 求集合[1..n]中和为sum的子集的个数 */
int calc(int n, int sum) {
if (dp[n][sum] != -1) //递归的终点: n==0 or 我们已经计算过了
return dp[n][sum];
/* 直接计算,将结果储存在dp数组中 */
else if (n > sum)
dp[n][sum] = calc(n - 1, sum);
else
dp[n][sum] = calc(n - 1, sum) + calc(n - 1, sum - n);
return dp[n][sum]; //返回计算的结果
}
int main() {
int n, sum;
scanf("%d", &n);
/* 没有这样子的子集合 */
if ((n + 1) * n % 4 != 0) {
printf("0\n");
return 0;
}
/* 设置dp数组的初始情况 */
memset(dp, -1, sizeof(dp)); //数组初始化为-1
dp[0][0] = 1; //集合{0}中和为0的子集的个数为1
for (int j = 1; j < maxY; j++)
dp[0][j] = 0; //集合{0}中和大于0的子集的个数为0
sum = (n + 1) * n / 4; //子集合需要满足的部分和
printf("%d\n", calc(n, sum) / 2);
}
有任何问题欢迎评论交流,如果本文对您有帮助不妨点点赞,嘻嘻~
欢迎关注个人公众号“ 鸡翅编程 ”,这里是认真且乖巧的码农一枚。
---- 做最乖巧的博客er,做最扎实的程序员 ----
旨在用心写好每一篇文章,平常会把笔记汇总成推送更新~