P1043 [NOIP 2003 普及组] 数字游戏
题目描述
丁丁最近沉迷于一个数字游戏之中。这个游戏看似简单,但丁丁在研究了许多天之后却发觉原来在简单的规则下想要赢得这个游戏并不那么容易。游戏是这样的,在你面前有一圈整数(一共 nnn 个),你要按顺序将其分为 mmm 个部分,各部分内的数字相加,相加所得的 mmm 个结果对 101010 取模后再相乘,最终得到一个数 kkk。游戏的要求是使你所得的 kkk 最大或者最小。
例如,对于下面这圈数字(n=4n=4n=4,m=2m=2m=2):

要求最小值时,((2−1) mod 10)×((4+3) mod 10)=1×7=7((2-1)\bmod10)\times ((4+3)\bmod10)=1\times 7=7((2−1)mod10)×((4+3)mod10)=1×7=7,要求最大值时,为 ((2+4+3) mod 10)×(−1 mod 10)=9×9=81((2+4+3)\bmod10)\times (-1\bmod10)=9\times 9=81((2+4+3)mod10)×(−1mod10)=9×9=81。特别值得注意的是,无论是负数还是正数,对 101010 取模的结果均为非负值。
丁丁请你编写程序帮他赢得这个游戏。
输入格式
输入文件第一行有两个整数,nnn(1≤n≤501\le n\le 501≤n≤50)和 mmm(1≤m≤91\le m\le 91≤m≤9)。以下 nnn 行每行有个整数,其绝对值 ≤104\le10^4≤104,按顺序给出圈中的数字,首尾相接。
输出格式
输出文件有 222 行,各包含 111 个非负整数。第 111 行是你程序得到的最小值,第 222 行是最大值。
输入输出样例 #1
输入 #1
4 2
4
3
-1
2
输出 #1
7
81
说明/提示
【题目来源】
NOIP 2003 普及组第二题
代码及注释
cpp
#include <bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f; // 定义无穷大,用于DP初始化
int n, // 数字总个数
m; // 要分成的段数
int d[110]; // 存储环形数组,开两倍长度方便拆环
int s[110]; // 前缀和数组,s[i]表示前i个数的和
int min_dp[55][10]; // min_dp[i][j]:前i个数分成j段,各段和取模后乘积的最小值
int max_dp[55][10]; // max_dp[i][j]:前i个数分成j段,各段和取模后乘积的最大值
// DP转移核心思路:
// dp[i][j] = 前k个数分成j-1段的最优解 * 最后一段(k+1~i)的和
// 即:把问题拆成 前面j-1段 + 最后1段
int min_ans=inf; // 全局最小答案(环所有起点的最小值)
int max_ans=-inf; // 全局最大答案(环所有起点的最大值)
// 函数功能:以x为起点,拆出长度为n的链,进行DP计算
void go(int x){
memset(s,0,sizeof(s)); // 前缀和数组初始化
// 计算以x为起点的链的前缀和
for(int i=1;i<=n;i++)s[i]=s[i-1]+d[x+i-1];
// DP数组初始化:最小值=无穷大,最大值=无穷小
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)min_dp[i][j]=inf,max_dp[i][j]=-inf;
// 边界条件:前i个数分成1段,乘积 = 这段和取模10的结果
for(int i=1;i<=n;i++)min_dp[i][1]=max_dp[i][1]=(s[i]%10+10)%10;
// DP状态转移
for(int j=2;j<=m;j++) // 枚举分成j段,从2段开始递推
for(int i=j;i<=n;i++) // 前i个数,j段至少需要j个数
for(int k=j-1;k<i;k++){ // 枚举分割点:前k个数分j-1段,最后一段k+1~i
int val = ((s[i]-s[k])%10+10)%10;// 计算最后一段的和,并取模10(保证非负)
// 转移:更新最小/最大乘积
min_dp[i][j]=min(min_dp[i][j],min_dp[k][j-1]*val);
max_dp[i][j]=max(max_dp[i][j],max_dp[k][j-1]*val);
}
// 更新全局答案
min_ans=min(min_ans,min_dp[n][m]);
max_ans=max(max_ans,max_dp[n][m]);
}
int main(){
cin>>n>>m;
// 环形数组处理:复制一遍,方便从任意起点拆成链
for(int i=1;i<=n;i++){cin>>d[i];d[i+n]=d[i];}
// 枚举环的每一个起点,都做一次DP
for(int i=1;i<=n;i++)go(i);
// 输出最终答案
cout<<min_ans<<endl<<max_ans;
return 0;
}
核心问题
给定环形数组,将其划分为 m 个连续非空段,每段和对 10 取模(保证非负),求所有分段方案中:模值乘积的最小值 & 最大值。
关键预处理(环形数组处理)
环形数组无法直接 DP,破环成链:
把原数组复制一份接在后面(d[i+n]=d[i]);
枚举每一个起点,截取长度为 n 的链,单独做线性 DP;
最终取所有起点的 DP 结果的全局最值。
动态规划思路
- 状态定义
min_dp[i][j]:前 i 个数 分成 j 段,乘积最小值
max_dp[i][j]:前 i 个数 分成 j 段,乘积最大值 - 边界条件
分成1 段时:前 i 个数就是一段,乘积 = 这段和的模值
min_dp[i][1] = max_dp[i][1] = (和%10+10)%10 - 状态转移方程(代码三循环逻辑)
dp[i][j] = max_min( dp[k][j-1] * 第k+1~i段的模值 )
含义:前 k 个数分j-1段 + 最后一段k+1~i,拼接成i个数分j段
约束:j-1 ≤ k < i(j-1 段至少需要 j-1 个数) - 三循环顺序
这是线性划分 DP的标准循环:
第一层:枚举分段数 j(从 2 到 m,1 段是边界)
第二层:枚举总数字数 i(j 段至少需要 i≥j 个数)
第三层:枚举分割点 k(前 k 个数分 j-1 段,k∈[j-1, i-1])
四、代码核心规则(边界 / 计算)
合法性约束:j 段至少需要 j 个数字,分割点 k 最少为j-1
取模处理:(sum%10+10)%10 保证结果非负
初始化:最小值初始化为无穷大,最大值初始化为无穷小
全局答案:遍历环形所有起点,合并所有 DP 结果得到最终答案
总结
- 本题是线性划分型 DP(非传统区间 DP),核心是将 i 个数划分为 j 段的最优解;
- 环形数组处理:破环成链,枚举所有起点做 DP;
- 三循环固定顺序:分段数 j → 总数字数 i → 分割点 k;
- 转移逻辑:j段最优解 = j-1段最优解 × 最后一段的模值;
- 边界:1 段为初始状态,严格遵守j段至少j个数的约束。