[DP] 整数划分 题解
题目描述
如何把一个正整数N(N长度<20)划分为M(M>1)个部分,使这M个部分的乘积最大。N、M从键盘输入,输出最大值及一种划分方式。
输入格式
第一行一个正整数T(T<=10000),表示有T组数据。
接下来T行每行两个正整数N,M。
输出格式
对于每组数据
第一行输出最大值。
第二行输出划分方案,将N按顺序分成M个数输出,两个数之间用空格格开。
样例
样例输入
1
199 2
样例输出
171
19 9
题解
拿到题第一眼,这不典型的DFS吗;
看了眼数据范围。。。
果断放弃DFS;
仔细读题,发现有“划分次数”这样带有明显阶段性的词语,很难不让人想到DP;
将一个整数划分成一个个区间,这提示了我们DP的种类:区间DP(其实种类知道不知道都一样);
定义f[i][j]表示前i位截了j次所能达到的最大价值;
初始化$$f[i][1] = a[1][i]$$其中a[i][j]表示从第i位到第j位的数;
例如,对于数12345,a[3][5] == 345;
状态转移方程$$f[i][j] = max(f[i][j], f[k][j - 1] * a[k + 1][i])$$ 其中k为断点,需要从1到i - 1枚举(很简单,不再详细解释);
我们发现,a数组需要预处理出来,其实可以采用区间DP的思想,在这里我采用的是一个非常麻烦的个人思路,但也能出来正确结果;
接下来就是输出路径的问题了;
定义g[i][j]表示前i位划分j次如果要达到最大值需要的断点,每次更新f数组时顺便将g数组更新;
输出时,我们只需从m开始倒序遍历,每划分一次就输出一次,同时更新总长度的值(因为已经输出的值对于我们来说是无用的),这样就可以输出路径;
具体看代码
代码
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
long long n, m;
int t;
long long a[1005][1005];
long long f[1005][1005]; //长度,次数;
long long g[1005][1005];
int b[105];
int c[105];
int main() {
cin >> t;
for (int pp = 1; pp <= t; pp++) {
cin >> n >> m;
memset(a, 0, sizeof(a));
memset(f, 0, sizeof(f));
memset(b, 0, sizeof(b));
memset(c, 0, sizeof(c));
memset(g, 0, sizeof(g));
long long o = 1; //输入的数的总位数
while(n) {
b[o] = n % 10;
n /= 10;
o++;
}
o--;
for (int i = 1; i <= o; i++) {
c[i] = b[o - i + 1];
}
for (int i = 1; i <= o; i++) {
for (int j = 1; j <= o; j++) {
int p = i;
for (int k = j - i + 1; k >= 1; k--) {
a[i][j] += (pow(10, k)) * c[p];
p++;
}
a[i][j] /= 10;
}
} //上面都是预处理数组a;
for (int i = 1; i <= o; i++) {
f[i][1] = a[1][i]; //初始化;
}
for (int j = 2; j <= m; j++) {
for (int i = 1; i <= o; i++) {
for (int k = 1; k < i; k++) {
if (f[i][j] <= f[k][j - 1] * a[k + 1][i]) {
f[i][j] = f[k][j - 1] * a[k + 1][i];
g[i][j] = k;
}
}
}
}
cout << f[o][m] << endl;
long long ans[10005] = {0};
int t = 0;
for (int i = m; i >= 1; i--) {
ans[++t] = a[g[o][i] + 1][o]; //这里的g[o][i]要+1是因为我们的a数组是包含左右端点的;
o = g[o][i]; //减小长度,已经输出的对我们来说无用;
}
for (int i = t; i >= 1; i--) {
cout << ans[i] << ' ';
}
cout << endl;
}
return 0;
}