题目信息
题目来源:CCF 天津省选 2009;
在线评测地址:Luogu#3860;
运行限制:时间不超过 (1.00 extrm{s}),空间不超过 (128 extrm{MiB})。
题目背景
你应火星人之邀为他们设计一款新型的手机。我们知道在标准的地球人手机上,数字键共有 (10) 个,(26) 个字母 a
…z
分别与某个数字键相关联,并且一个数字键上的若干字母必须是字母表中连续的一段。比如下图是地球手机的一个标准方案:
题目描述
我们要输入一个字母,必须连续按它所在的数字键若干次,次数即为这个字母在这个键的第几个位置。例如在上图的方案中,若我们要输入 C
,就需要按三次数字键 2
;若要输入 M
,需按一次数字键 6
。
火星人手机的构造与地球人手机类似,上面有 (M) 个火星数字键,你需要把火星文的 (N) 个字母放置在这 (M) 个键上。(同样要求一个数字键上必须是连续的若干个火星字母)
现在给定一段火星文中各个字母的出现次数,你设计的手机必须使得输入这段文字所需的按键次数最少。
输入格式
输入文件的第一行包括两个数字 (N) 和 (M),分别表示火星文字母数和火星手机的按键数。接下来有 (N) 行,每行包含一个数字,依次表示每个字母在文章中的出现次数。这个次数不超过 (1000)。
输出格式
输出文件的第一行包括一个数字,表示最少的按键次数。
接下来的 (M) 行表示一种设计方案:每行包含一个数,依次表示每个数字键上有几个火星字母。(这些数字可以为 (0))
如果有多种方案可以得到最少的按键次数,你需要输出第一个数字键上包含字母最少的方案;如果仍有多种方案,你需要在其中选择第二个数字键上字母最少的方案;依此类推。
数据规模及约定
对于 (100\%) 的数据,(1le Nle 500),(1le Mle 100)。
分析
题意是说给定 (N) 个数,将其分成连续的 (M) 组,使 (sumlimits_{i=1}^M v_i(i-l_i+1)) 最小,其中 (v_i) 是第 (i) 点的权值,而 (l_i) 是第 (i) 个数所在区间的左端点。并给出一组字典序最小的答案。
既然都分组了,还是连续的,显然是 DP。令 (f_{i,j}) 为将前 (i) 个数分成 (j) 组的答案。则 (f_{i,j}=min_k{f_{k,j-1} + g(k+1,i)}),其中 (g(x,y)=sumlimits_{i=x}^y v_i(i-x+1))。
注意到 (g(x,y)=sumlimits_{i=x}^y v_i(i-x+1)=sumlimits_{i=x}^{y} iv_i-(x-1)sumlimits_{i=x}^y v_i),可以前缀和优化。
这样,枚举所有 (f_{i,j}),然后枚举 (k) 转移,复杂度就是 (mathcal{O}(N^2M)),可以 AC。
注意事项
初始时,(f_{0,j}=0),其余应该全部赋成 (infty)。
Code
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
const int max_n = 100, max_m = 500;
int cnt[max_m], com[max_n][max_m], out[max_n];
ll dp[max_n+1][max_m+1], pf1[max_m+1], pf2[max_m+1]; // DP 数组和前缀和
ll getval(int l, int r) { return (pf1[r+1] - pf1[l]) - (pf2[r+1] - pf2[l]) * l; } // 相当于 g 函数
int main()
{
memset(dp, 0x3f, sizeof(dp)); // 初始化 1
int n, m;
scanf("%d%d", &m, &n);
for (int i = 0; i < m; i++)
scanf("%d", cnt + i); // 输入
pf1[0] = pf2[0] = 0;
for (int i = 0; i < m; i++) // 统计前缀和
{
pf1[i+1] = pf1[i] + cnt[i] * (i + 1);
pf2[i+1] = pf2[i] + cnt[i];
}
for (int i = 0; i <= n; i++) // 初始化 2
dp[i][0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) // dp[i][j]
for (int k = 0; k < j; k++) // 枚举转移点
if (dp[i][j] > dp[i-1][k] + getval(k, j-1))
dp[i][j] = dp[i-1][k] + getval(k, j-1), com[i-1][j-1] = k; // 取最大值
printf("%lld", dp[n][m]); // 先输出答案
for (int i = n - 1, j = com[n-1][m-1], k = m; i >= 0; i--, k = j, j = com[i][k-1]) // 反向统计转移点
out[i] = k - j;
for (int i = 0; i < n; i++) // 倒序后输出即可
printf("
%d", out[i]);
return 0; // 然后就 AC 了、
}