【题解】[TJOI2009] 火星人的手机
题目信息
题目来源:CCF 天津省选 2009;
在线评测地址:Luogu#3860;
运行限制:时间不超过 \(1.00\ \textrm{s}\),空间不超过 \(128\ \textrm{MiB}\)。
题目背景
你应火星人之邀为他们设计一款新型的手机。我们知道在标准的地球人手机上,数字键共有 \(10\) 个,\(26\) 个字母 a
…z
分别与某个数字键相关联,并且一个数字键上的若干字母必须是字母表中连续的一段。比如下图是地球手机的一个标准方案:
题目描述
我们要输入一个字母,必须连续按它所在的数字键若干次,次数即为这个字母在这个键的第几个位置。例如在上图的方案中,若我们要输入 C
,就需要按三次数字键 2
;若要输入 M
,需按一次数字键 6
。
火星人手机的构造与地球人手机类似,上面有 \(M\) 个火星数字键,你需要把火星文的 \(N\) 个字母放置在这 \(M\) 个键上。(同样要求一个数字键上必须是连续的若干个火星字母)
现在给定一段火星文中各个字母的出现次数,你设计的手机必须使得输入这段文字所需的按键次数最少。
输入格式
输入文件的第一行包括两个数字 \(N\) 和 \(M\),分别表示火星文字母数和火星手机的按键数。接下来有 \(N\) 行,每行包含一个数字,依次表示每个字母在文章中的出现次数。这个次数不超过 \(1000\)。
输出格式
输出文件的第一行包括一个数字,表示最少的按键次数。
接下来的 \(M\) 行表示一种设计方案:每行包含一个数,依次表示每个数字键上有几个火星字母。(这些数字可以为 \(0\))
如果有多种方案可以得到最少的按键次数,你需要输出第一个数字键上包含字母最少的方案;如果仍有多种方案,你需要在其中选择第二个数字键上字母最少的方案;依此类推。
数据规模及约定
对于 \(100\%\) 的数据,\(1\le N\le 500\),\(1\le M\le 100\)。
分析
题意是说给定 \(N\) 个数,将其分成连续的 \(M\) 组,使 \(\sum\limits_{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)=\sum\limits_{i=x}^y v_i(i-x+1)\)。
注意到 \(g(x,y)=\sum\limits_{i=x}^y v_i(i-x+1)=\sum\limits_{i=x}^{y} iv_i-(x-1)\sum\limits_{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("\n%d", out[i]);
return 0; // 然后就 AC 了、
}
本文来自博客园,作者 5ab,转载请注明链接哦 qwq
博客迁移啦,来看看新博客吧 -> https://5ab-juruo.oier.space/