【题解】[TJOI2009] 火星人的手机

题目信息

题目来源:CCF 天津省选 2009;

在线评测地址:Luogu#3860

运行限制:时间不超过 \(1.00\ \textrm{s}\),空间不超过 \(128\ \textrm{MiB}\)

题目背景

你应火星人之邀为他们设计一款新型的手机。我们知道在标准的地球人手机上,数字键共有 \(10\) 个,\(26\) 个字母 az 分别与某个数字键相关联,并且一个数字键上的若干字母必须是字母表中连续的一段。比如下图是地球手机的一个标准方案:

本图由洛谷图床搬运

题目描述

我们要输入一个字母,必须连续按它所在的数字键若干次,次数即为这个字母在这个键的第几个位置。例如在上图的方案中,若我们要输入 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 了、
}
posted @ 2020-08-29 16:51  5ab  阅读(104)  评论(0编辑  收藏  举报