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

题目信息

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

在线评测地址:Luogu#3860

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

题目背景

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

本图由洛谷图床搬运

题目描述

我们要输入一个字母,必须连续按它所在的数字键若干次,次数即为这个字母在这个键的第几个位置。例如在上图的方案中,若我们要输入 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 了、
}
原文地址:https://www.cnblogs.com/5ab-juruo/p/solution-tjoi2009_lg3860.html