11090 最大m段乘积和最小m段和(优先做)
时间限制:1000MS 代码长度限制:10KB
提交次数:0 通过次数:0
题型: 编程题 语言: G++;GCC;VC;JAVA
Description
一个n位十进制整数S,若将S划分为m个段,则可以得到m个整数。
(1)这m个整数的乘积称为S的一个“m段乘积”,对于给定的S和m,求S的最大m段乘积。
(2)这m个整数的和称为S的一个“m段和”,对于给定的S和m,求S的最小m段和。
输入格式
输入:三个整数,n,m,S。
第一个n表示S的位数,第二个m表示分割的段数,第三个数为需要被分段的n位十进制数S。
n、m和S三个数中间空格相连,这里1<=m<=n,n<=10,即S、S的最大m段乘积、S的最小m段和这三个
数都用int型即可,虽然输出的数可能很大,但这里32位整数够了,测试数据没有超过32位整数,即
无需考虑多位的高精度数。
例如,十进制数3456的“最大3段乘积”为1020。因为3456划分3个段有如下情形:
3456=672,3456=810,3456=1020。
3456的“最小3段和”为45,因为3+4+56=63,3+45+6=54,34+5+6=45。
输出格式
输出:
第一行计算出的最大m段乘积和最小m段和,中间空格相连。
第二行写出最大m段乘积的乘法表达式。
第三行写出最小m段和的加法表达式。
这里约定:
(1)若有多种分段方法使得最大m段乘积相等且都最大,则优先输出靠左的段更短小的这种方式。
比如输入3 2 111,这里最大m段乘积的乘法表达式为:111=11,而不要输出111=11
(2)若分出的某段数字有0开头的,不输出0。
比如输入5 3 20001,这里输出:201=0,(其实代表着:20001,这里001,只写1即可)
(3)若只分一个段,表达式也就写1个段的数等于某个数。
比如输入2 1 12,输出:12=12
(4)最小m段和的加法表达式也同样满足前面的约定(1)(2)(3)。
输入样例
4 3 3456
输出样例
1020 45
3456=1020
34+5+6=45
解题思路
1. dp 方程定义
由于 f 和 t 分析过程一样,所以下面只对 f 进行分析(即最大 m 段乘积和)
- f[i][j]:前 i 个数划分为 j 段时的最大乘积,f[n][m] 为所求
- fs[i][j]:前 i 个数划分为 j 段时各个最大乘积的分割位置
- fa[i]:存储最大 m 段乘积的每个值(组成部分)
2. 状态转移方程
设 w(h,t) 是 S 从 h 位开始的共 t 位数字组成的十进制数,约定从1开始从左向右对位进行编号,即最左一位定为第1位。
对于 f(i, j),这里只考虑 i>=j 的情况,因为每个段至少1位,因此 i 必然大于等于 j ,因为比如3位数字(i),你最多只能分为3、2、1段(j),即 i >= j。
边界
-
当 j = 1 时,f(i, 1) = w(1, i), 1 <= i <= n 表示当只划分1个段时,最大段乘积
就是从第 1 位开始共 i 位数(i>=1)的十进制数值。 -
当 j>=2 时,计算 f(i, j) 的动态规划的递归式如下:f(i,j) = max{ f(k, j-1) * w(k+1, i-k) | k from j-1 to i-1 } j >= 2 && j <= i <= n
(即让 k 从 j - 1 到 i - 1 变化,找 f(k, j-1) 和 w(k + 1, i - k) 乘积的最大值)
这个公式这样理解:
当 j >= 2 && j <= i <= n 时,现在要求解的问题是前i位划分为 j 个段的最大 j 段乘积,
这时考虑前 k 位,划分 j - 1 个段(因为最后一个段至少占1位,而前 j - 1 个段又至少有 j - 1 位,所以 j-1 <= k <= i-1),先获得这 j - 1 个段的最大段乘积(从前 k 个数中选),再乘以从第 k + 1 位到第 i 位(共 i - k 位,因为前 i 个数,从 k 开始)的十进制数。
即让 k 从 j - 1 到 i - 1 循环,求 f(k, j-1) 和 w(k+1, i-k) 乘积的最大值。
for (int j = 2; j <= m; j++) {
for (int i = j; i <= n; i++) {
f[i][j] = f[j - 1][j - 1] * w(j, i - j + 1);
fs[i][j] = j;
t[i][j] = t[j - 1][j - 1] + w(j, i - j + 1);
ts[i][j] = j;
for (int k = j; k <= i - 1; k++) {
int tMax = f[k][j - 1] * w(k + 1, i - k);
if (f[i][j] < tMax) {
f[i][j] = tMax;
fs[i][j] = k + 1;
}
int tMin = t[k][j - 1] + w(k + 1, i - k);
if (t[i][j] > tMin) {
t[i][j] = tMin;
ts[i][j] = k + 1;
}
}
}
}
对于求如何获得“最大m段乘积的乘法表达式”
例如 3456 分割成 34 * 5 * 6 = 1020
- 通过求 f 时不断记录索引位置到 fs 数组,然后通过遍历 fs 来不断获得各个数字段并记录到 fa,即 fa[j] = w(fs[i][j], k - fs[i][j]);
- 其中 fs[i][j] 为前 i 个数划分为 j 段时求出的索引位置;
- k 为这一段数值结束位置,因此对于 k,从后往前遍历,每次循环都将 k 赋值为上一次的断点(即 k = fs[i][j]);
- 同时 i 要更新成上一次断点的前一位数,毕竟 i 的含义是前 i 个数(即 i = fs[i][j] - 1 ),由于断点处开始的后面部分已经记录完了,那就往断点处前面继续开始找即可,比如 3456,断点为数字5,则后面的第三位和第四位已经被记录过了,下一次的前 i 个数字从数字4开始。
之后输出 f[a] 中的数字即可
int i = n;
int k = n + 1;
for (int j = m; j >= 1; j--) {
fa[j] = w(fs[i][j], k - fs[i][j]);
k = fs[i][j]; //记录上次的断点
i = fs[i][j] - 1; // 由于断点处开始的后面部分已经记录完了,那就往断点处前面继续开始找即可,比如 3456,断点为数字5,则后面的第三位和第四位已经被记录过了,下一次的前 i 个数字从数字4开始
}
for (int i = 1; i <= m; i++) {
cout << fa[i];
if (i != m)
cout << "*";
}
cout << "=" << f[n][m] << endl;
最小m段和的动态规划递归式
最小m段和公式的分析和最大m段乘积的公式分析是相同的。
边界: 当j=1时,t(i,1) = w(1,i), 1<=i<=n 表示当只划分1个段时,最小段和就是从第1位开始共i位
数(i>=1)的十进制数值。
当j>=2时,计算t(i,j)的动态规划的递归式如下:
t(i,j) = min{ t(k, j-1) + w(k+1, i-k) | k from j-1 to i-1 } j>=2 && j<=i<=n
(即让k从j-1到i-1变化,找t(k, j-1)和w(k+1, i-k)之和的最小值)
更多注释可查看下方的完整代码中,有助于理解
代码如下
#include <iostream>
using namespace std;
int f[11][11]; //最大 m 段乘积,f[n][m] 为所求
int fs[11][11]; //存储最大 m 段乘积分割位置
int fa[11]; //存储最大 m 段乘积的每个值
int t[11][11]; //最小 m 段和,t[n][m] 为所求
int ts[11][11]; //存储最小 m 段和分割位置
int ta[11]; //存储最小 m 段和的每个值
int num[11]; // 存储字符串各个值
int w(int h, int t) {
int result = 0;
for (int i = 0; i < t; i++) {
result = result * 10 + num[h + i];
}
return result;
}
int main() { //最大m段乘积,最小m段和
int n, m, s;
cin >> n >> m >> s;
int ss = s;
for (int i = n; i >= 1; i--) {
num[i] = ss % 10;
ss = ss / 10;
}
for (int i = 1; i <= n; i++) {
f[i][1] = w(1, i);
ts[i][1] = 1;
t[i][1] = w(1, i);
fs[i][1] = 1;
}
for (int j = 2; j <= m; j++) {
for (int i = j; i <= n; i++) {
f[i][j] = f[j - 1][j - 1] * w(j, i - j + 1);
fs[i][j] = j;
t[i][j] = t[j - 1][j - 1] + w(j, i - j + 1);
ts[i][j] = j;
for (int k = j; k <= i - 1; k++) {
int tMax = f[k][j - 1] * w(k + 1, i - k);
if (f[i][j] < tMax) {
f[i][j] = tMax;
fs[i][j] = k + 1;
}
int tMin = t[k][j - 1] + w(k + 1, i - k);
if (t[i][j] > tMin) {
t[i][j] = tMin;
ts[i][j] = k + 1;
}
}
}
}
cout << f[n][m] << " " << t[n][m] << endl;
int i = n;
int k = n + 1;
for (int j = m; j >= 1; j--) {
fa[j] = w(fs[i][j], k - fs[i][j]);
k = fs[i][j]; //记录上次的断点
i = fs[i][j] - 1; // 由于断点处开始的后面部分已经记录完了,那就往断点处前面继续开始找即可,比如 3456,断点为数字5,则后面的第三位和第四位已经被记录过了,下一次的前 i 个数字从数字4开始
}
for (int i = 1; i <= m; i++) {
cout << fa[i];
if (i != m)
cout << "*";
}
cout << "=" << f[n][m] << endl;
i = n;
k = n + 1;
for (int j = m; j >= 1; j--) {
//cout << "i "<< i << " j " << j << " " << ts[i][j] << endl;
ta[j] = w(ts[i][j], k - ts[i][j]);
k = ts[i][j]; //记录上次的断点
i = ts[i][j] - 1; //新的i值要减1
}
for (int i = 1; i <= m; i++) {
cout << ta[i];
if (i != m)
cout << "+";
}
cout << "=" << t[n][m] << endl;
return 0;
}
最后
对我感兴趣的小伙伴可查看以下链接
- 我的掘金主页:https://juejin.cn/user/1302297507801358
- 博客主页:http://blog.zhangjiancong.top/
- 公众号:Smooth前端成长记录