1. 程式人生 > 實用技巧 >【題解】[TJOI2009] 火星人的手機

【題解】[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 了、
}