问题 1531: [蓝桥杯][算法提高VIP]数的划分(搜索+剪枝+记忆化搜索DFS 动态规划DP)

本文深入探讨了数的划分问题,提供了多种算法解决方案,包括深度优先搜索、动态规划及记忆化搜索等,对比分析了各方法的优劣,并详细解释了关键代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题的链接:问题 1531: [蓝桥杯][算法提高VIP]数的划分

重点: dfs判重:保存上一次值,下一次循环时要小于上一次值就行,即最后序列是一个递减序列;或者下一次循环大于上一次值,即最后序列是一个递增序列。
参考代码1.0: 比较笨,通过位数构造序列,最后判断是不是n的大小。。。超时代码!!!
#include <string>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

int n, pos, k;
int s[10010];

//当前位数 需要位数 存储上一次数的大小
//判重 1 1 2 和 2 1 1 可以用第三个参数来保证其序列永远是从大到小即2 1 1
void dfs(int a, int k, int b)
{
	if(a == k)
	{
	    int sum = 0;
	    //求和
	    for(int i = 0; i < k; i++) sum += s[i];
	    //和为n时输出并计数
	    if(sum == n)
        {
            for(int i = 0; i < k; i++)
            {
                if(i == 0) cout << s[i];
                else cout << "+" << s[i];
            }
            cout << endl;
            pos ++;
        }
		return;
	}
	for(int i = 1; i <= n && i <= b; i++)
	{
		s[a] = i;
		dfs(a + 1, k, i);
	}
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++) dfs(0, i, n);
	cout << pos << endl;
	return 0;
}

参考代码2.0: 用第一个参数存储当前值,减为0则说明构造成功一组,进行输出;(累减为0)
#include <string>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

int n, pos, k;
int s[10010];

//当前数 上次最大数(判重) 当前数组下标
void dfs1(int a, int k, int j)
{
    if(a == 0)
	{
        for(int i = 0; i < j; i++)
        {
            if(i == 0) cout << s[i];
            else cout << "+" << s[i];
        }
        cout << endl;
        pos ++;
		return;
	}
    for(int i = 1; i <= k && i <= a; i++)
	{
		s[j] = i;
		dfs1(a - i, i, j + 1);
	}
}

int main()
{
	cin >> n;
	dfs1(n, n, 0);
	cout << pos << endl;
	return 0;
}

参考代码3.0: 类似2.0,这个是(累加为n)
#include <string>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

int n, pos, k;
int s[10010];

void dfs2(int a, int k, int j)
{
	if(a == n)
	{
        for(int i = 0; i < j; i++)
        {
            if(i == 0) cout << s[i];
            else cout << "+" << s[i];
        }
        cout << endl;
        pos ++;
		return;
	}
	for(int i = 1; i <= n - a && i <= k; i++)
	{
		s[j] = i;
		dfs2(a + i, i, j + 1);
	}
}

int main()
{
	cin >> n;
    dfs2(0, n, 0);
	cout << pos << endl;
	return 0;
}

注意: 前面的搜索超时代码的dfs,所以应该避免走重复子问题,用到了标记,记忆化搜索;

参考代码4.0: vis[m][k]为把m分成k份的种类数,将每种情况都存储下来,下次dfs到他的时候就可以直接取用而不是继续去重复的浪费时间去递归。
  • m < k 时,即盒子多,球少,没法构造成k位数,则直接返回0;
  • 盒子为1时,无论球多少,只有一种情况,即{m};
  • 盒子与球相等时,则只有一种情况,每个盒子只能放一个球,即{1,1,1,1,1…}!
  • 否则就是球数大于盒子数分为两种情况
  1. 有1的序列,即至少有一个1,即dfs(m - 1, k - 1),从m中拿一个球放到k中,再将m - 1个球放到剩下的k - 1个盒子里;
  2. 没有1的序列,即每个盒子里至少大于等于2个球,即dfs(m - k, k), 先从m 个球中取 k 个球放到 k 个盒子里,再将剩下的m - k 个球放到 k 个盒子里, 这样就能保证每个盒子至少两个球,若m - k 后的值 小于k 则相当于 dfs(m, k) = dfs(m - 1, k - 1) + dfs(m - j ,k),即最后变为 dfs(m, k) = dfs(m - 1, k - 1) + 0;
所以最终递推公式:dfs(m, k) = dfs(m - 1, k - 1) + dfs(m - j ,k)
#include <string>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

int n, sum, k;
int vis[1010][1010];

//将m个求放到k个盒子
int dfs(int m, int k)
{
    if(vis[m][k]) return vis[m][k];
    //球数小于盒子数无法填满盒子,返回0
    if(m < k) return 0;
    //盒子数为1和盒子数为球数只有一种情况
    if(k == 1 || k == m) return 1;
    //球数大于盒子数则递归
    return vis[m][k] = dfs(m - k, k) + dfs(m - 1, k - 1);
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++) sum += dfs(n, i);
	cout << sum << endl;
	return 0;
}

参考代码5.0: 动态规划DP, 按位计算,最后累加。

若是不好想,也可以以这种方式推到公式:也可以作为4.0推论的另一种解释
我们用dp[i][j]来表示把i划分为j份的方法,直接想方程不好想,我假设已经划分好了i
i = a1 + a2 + … + aj
不妨把每一个a都减去1
i-j = (a1-1) + (a2-1) + a(aj-1)
这时分法与原来一样
dp[i][j] = dp[i - j][1] + dp[i - j][2] +…+ dp[i - j][j - 1] + dp[i - j][j]
dp[i - 1][j - 1] = dp[i - j][1] + dp[i - j][2]… + dp[i - j][j - 1]
由上面两式推出
dp[i][j] = dp[i - 1][j - 1] + dp[i - j][j]

#include <string>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

int n, pos;
int dp[1010][1010];

int main()
{
	cin >> n;
    dp[0][0] = 1;
    for(int i = 1; i <= n; i++)//数
        for(int j = 1; j <= n && j <= i; j++)//盒子
             dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1];
    for(int i = 1; i <= n; i++) pos += dp[n][i];
    cout << pos << endl;
	return 0;
}

参考代码6.0: 动态规划DP,dp[i][j]表示把 i 拆分成不超过 j 的方案数,和5.0不同,目前我没有看懂!!!。。。。
转移方程:dp[i][j] = dp[i][j - 1] + dp[i - j][j]
#include <string>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

int n, pos;
int dp[1010][1010];

int main()
{
	int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) dp[i][0]=0;
    for(int i=0;i<=n;i++) dp[0][i]=1;

    //dp[i][j]表示把i拆分成不超过j的方案数
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            //dp[i][j]只有包含j和不包含j两种情况
            //dp[i][j-1]不包含j,dp[i-j][j]包含若干个j
            if(i>=j) dp[i][j]=dp[i][j-1]+dp[i-j][j];
            else dp[i][j]=dp[i][i];
        }
    }
    printf("%d",dp[n][n]);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值