3428.放苹果-AcWing
3428.放苹果:把 M 个同样的苹果放在 N 个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?盘子相对顺序不同,例如 5,1,1 和 1,5,1 算作同一种分法。
解法1:递归划分
int distribute(int apples,int plates);返回apples装入plates中有多少种分配方法。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int distribute(int apples,int plates){
//递归终止条件
if(apples==0)return 1;
if(plates==1)return 1;
if(apples<plates)return distribute(apples,apples);//苹果数小于盘子数时等价于必然有plates-apples个空盘子,也即将apples放到apples中
else return distribute(apples-plates,plates)+distribute(apples,plates-1);//对立事件 要么每个盘子都装一个,要么至少有一个盘子不装
}
int main(){
int plates;
int apples;
cout<<"请分别输入苹果和盘子的数量\n";
cin>>apples>>plates;
cout<<"分配结果:\n共"<<distributeApples(apples,plates)<<"种分配结果\n";
}
解法2:动态规划法
dp[i][j]:表示i个苹果分到j个盘子的分法。则要对边界i=0或者j=1置初值
问:为什么不讨论j=0?答:0个盘子在后续动态转过程中也即for循环中不会被访问:for循环遍历范围为图片的阴影区域。蓝色区域为初始化的边界数据

#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
int main(){
int plates;
int apples;
cout<<"请分别输入苹果和盘子的数量\n";
cin>>apples>>plates;
vector<vector<int>> dp(apples+1,vector<int>(plates+1,0));
//边界条件初始化
for(int i=0;i<apples+1;i++)dp[i][1]=1;//不论多少个苹果,当只有一个盘子时分配结果只有一种
for(int j=0;j<plates+1;j++)dp[0][j]=1;//不论多少个盘子,当只有0个苹果时分配结果只有一种
//动态转移:
for(int i=1;i<apples+1;i++)
for(int j=2;j<plates+1;j++){
if(i<j)dp[i][j]=dp[i][i];
else dp[i][j]=dp[i-j][j]+dp[i][j-1];
}
cout<<"分配结果:\n共"<<dp[apples][plates]<<"种分配结果\n";
return 0;
}
解法3:回溯法求出每一种划分方式
//1,5,1和5,1,1是一样的分法
//那是不是可以用回溯法呢?好像不太行 当前盘子(plateIndex)分入苹果的个数i的可选范围是0~left个 下一个盘子(plateIndex+1)可分入个数为0~left-i个
//但会出现重复的5,0,0和0,5,0和0,0,5 变成了全排列问题,可以对ans去重得到最终结果
//如果想去重可以使用容器 set(利用集合的互异性) 但是vector<int>相同的定义是对应下标元素相同
//所以可以先对path排序再将path(vector<int> path)在插入ans(set<vector<int>> ans)
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
#include<set>
using namespace std;
set<vector<int>> ans;
int plates;
int apples;
//platesIndex为当前处理盘子的下标,若共有5个盘子则取值为0~4
//distribution[]表示当前的分配情况 每个元素初值为0
void backtracking(int plateIndex, vector<int> distribution, int& left) {
if (plateIndex >= plates || left == 0) {
if (left == 0) {
//此处对distribution进行了排序,所以函数传入的参数distribution不能使用引用类型
//一开始就是想着节省空间而使用的引用类型导致输出了错误的结果
sort(distribution.begin(), distribution.end());
//利用了set元素的特异性实现了去重
ans.insert(distribution);
}
return;
}
for (int i = 0; i <= left; i++) {
if (left - i < 0)break;
else {
distribution[plateIndex] = i;
left -= i;
backtracking(plateIndex + 1, distribution, left);
distribution[plateIndex] = 0;
left += i;
}
}
}
int main() {
cout << "请分别输入苹果和盘子的数量\n";
cin >> apples >> plates;
vector<int> distribution(plates, 0);
backtracking(0, distribution, apples);
cout << "共有" << ans.size() << "种分配方式\n";
for (auto c : ans) {
for (auto a : c)
cout << a << " ";
cout << '\n';
}
}
解法四:同样使用回溯法但是不需要使用set集合,也不需要排序操作
使用的是: 可以重复用i,但是不能回头用0~i-1也即相对于原始for(i=0;i<=left;i++)变为现在的for(i=i;i<=left;i++)使后面的盘子分的个数至少是i(看for循环开始的下标和backtracking传入的参数)
#include<iostream>
#include<vector>
#include<algorithm>
#include<string>
#include<set>
using namespace std;
vector<vector<int>> ans;
int plates;
int apples;
//platesIndex为当前处理盘子的下标,若共有5个盘子则取值为0~4
//distribution[]表示当前的分配情况 每个元素初值为0
void backtracking(int plateIndex,int i,vector<int> distribution,int &left) {
if(plateIndex>=plates||left==0) {
//一开始此处仅使用left==0作ans更新的判断条件,会爆错。输出如下图4.1。而加入plateIndex>=plates后正确输出的原因是“重复用i但不回头”方法的
//含义为:后续的每次分配至少是i从而减少了i从0处增长而导致重复分配。但在为第一个盘子分配时,若for循环i=5,left变为0,再次递归,
//会提前进入终止条件(此时的plateIndex为0,没有为后面的盘子(标号为的1~paltes-1的盘子)分配苹果),
//且符合更新finalPath的条件从而插入了重复答案。因此改变finalPath也即ans的更新条件即可解决上述问题。
if(left==0&&plateIndex>=plates) {
ans.push_back(distribution);
}
return;
}
for(; i<=left; i++) {
if(left-i<0)break;
else {
distribution[plateIndex]=i;
left-=i;
backtracking(plateIndex+1,i,distribution,left);
distribution[plateIndex]=0;
left+=i;
}
}
}
int main() {
cout<<"请分别输入苹果和盘子的数量\n";
cin>>apples>>plates;
vector<int> distribution(plates,0);
backtracking(0,0,distribution,apples);
cout<<"共有"<<ans.size()<<"种分配方式\n";
for(auto c:ans){
for(auto a:c)
cout<<a<<" ";
cout<<'\n';
}
}
图4.1(错误输出):


浙公网安备 33010602011771号