题目
丁丁最近沉迷于一个数字游戏之中。这个游戏看似简单,但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易。游戏是这样的,在你面前有一圈整数(一共 n 个),你要按顺序将其分为 m 个部分,各部分内的数字相加,相加所得的 m 个结果对 1010 取模后再相乘,最终得到一个数 k。游戏的要求是使你所得的 k 最大或者最小。
例如,对于下面这圈数字(n=4,m=2):
要求最小值时,((2−1) mod 10)×((4+3) mod 10)=1×7=7,要求最大值时,为 ((2+4+3) mod 10)×(−1 mod 10)=9×9=81。特别值得注意的是,无论是负数还是正数,对 10 取模的结果均为非负值。
丁丁请你编写程序帮他赢得这个游戏。
输入
输入文件第一行有两个整数,n(1≤n≤50) 和 m(1≤m≤9)。以下 n 行每行有个整数,其绝对值 ≤10^4,按顺序给出圈中的数字,首尾相接。
输出
输出文件有 2 行,各包含 1 个非负整数。第 1 行是你程序得到的最小值,第 2 行是最大值。
输入样例
4 2
4
3
-1
2
输出样例
7
81
思路
- 先将环转换成链,即存两遍数组,如[4, 3, -1, 2]存为[4, 3, -1, 2, 4, 3, -1, 2];
- 利用前缀和辅助求特定区间内数字的和;
- 利用动态规划求最大值和最小值,s[i][j][k]表示区间[i,j]内分k段的最小情况,b[i][j][k]表示最大情况;
- 初始化:将任意区间值只分一段的情况先算出来,即将所有s[i][j][1] = mod(pre[j] - pre[i-1]),其中pre是前缀和数组,mod代表进行mod 10运算,b[i][j][1]做同样处理;除此之外,还要s[i][j][k](k>=2)都设为正无穷,为后面求最小值做准备,b数组不用,因为b数组默认都为0;
- 依次遍历分段数k(2 ≤ k ≤ m) ,左边界 i (1 ≤ i ≤ 2*n),右边界 j (i+k-1 ≤ j ≤ 2*n,因为要保证[i,j]内能分出k段,故起始点为i+k-1),[i,j]内的分段点d (i+k-2 ≤ d < j,要保证[i,d]之间能分出k-1段,故起始点为i+k-2);
- 最后遍历一次即可求得答案,minn = min(minn, s[i][i+n-1][m]),i 从 1遍历到n;maxx同理。
代码
#include <iostream>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int MAXN = 106;
int n, m;
int a[MAXN];
int pre[MAXN]; //前缀和数组
int b[MAXN][MAXN][10], s[MAXN][MAXN][10]; //最大和最小结果的dp数组,代表在区间[i,j]内分k段能获得的最大/最小值
int mod10(int t){
return ((t % 10) + 10) % 10;
}
void init()
{
for(int i = 1; i<=2*n; i++){
for(int j = i; j<=2*n; j++){
b[i][j][1] = s[i][j][1] = mod10(pre[j] - pre[i-1]); //初始化i和j之间只分一段的情况
}
}
for(int k = 2;k<=m;k++){
for(int i = 1;i<=2*n;i++){
for(int j = i + k - 1;j<=2*n;j++){
s[i][j][k] = INF; //初始化[i,j]内分多段时为最大值,为后面求最小值做准备
} //b数组不用初始化,是因为默认为0
}
}
}
int main()
{
cin >> n >> m;
for(int i = 1;i <= n;i++){
int gtg;
cin >> gtg;
a[i] = gtg;
a[i+n] =gtg; //环破坏成链
}
pre[0] = 0;
for(int i = 1; i<=2*n ;i++){
pre[i] = pre[i-1] + a[i]; //前缀和
//cout << pre[i] << endl;
}
init();
for(int k = 2;k<=m;k++){ //枚举分段数
for(int i = 1;i<=2*n;i++){ //枚举左边界
for(int j = i + k - 1;j<=2*n;j++){ //枚举右边界, 要保证[i,j]内能分出k段,故起始点为i+k-1;
for(int d = i + k - 2;d<j;d++){ //枚举分段点, 要保证[i,d]之间能分出k-1段,故起始点为i+k-2;
//s[i][j][k-1]已在前面的遍历中算出,故直接用
s[i][j][k] = min(s[i][j][k], s[i][d][k-1]*mod10(pre[j] - pre[d]));
b[i][j][k] = max(b[i][j][k], b[i][d][k-1]*mod10(pre[j] - pre[d]));
}
}
}
}
int minn = INF, maxx = 0;
for(int i = 1; i<=n;i++){
minn = min(minn, s[i][i+n-1][m]);
maxx = max(maxx, b[i][i+n-1][m]);
}
cout << minn << endl << maxx << endl;
return 0;
}
思考
- 为什么存两遍数组可行?
存两遍数组,解决了头尾连接问题,在四层遍历过程中,可以遍历到所有情况; - 为什么最后 i 只用从1遍历到n?
因为数据长度为n,只能是连续的n个数字,而左边界从 1 到 n ,右边界为 i+n-1,即可覆盖所有情况; - 为什么s[i][j][k] = min(s[i][j][k], s[i][d][k-1]*mod10(pre[j] - pre[d]))可行?
因为当遍历到s[i][j][k]时,必然已遍历过s[i][d][k-1],故可直接在此基础上计算[i,j]区间内,以d为断点,分k段的结果,并与现在的结果作比较,保留较小的结果。其中[i,d]中分k-1段,[d+1,r]看作1段,一共k段,且d < r,不能等于。