1. 程式人生 > 實用技巧 >題解 洛谷 P3294 [SCOI2016]背單詞

題解 洛谷 P3294 [SCOI2016]背單詞

題目

題目

鳳老師告訴 Lweb ,我知道你要學習的單詞總共有 n 個,現在我們從上往下完成計劃表,對於一個序號為 x 的單詞(序號 1...x-1 都已經被填入):
1. 如果存在一個單詞是它的字尾,並且當前沒有被填入表內,那他需要吃 n*n 顆泡椒才能學會;
2. 當它的所有後綴都被填入表內的情況下,如果在 1...x-1 的位置上的單詞都不是它的字尾,那麼你吃 x 顆泡椒就能記住它;
3. 當它的所有後綴都被填入表內的情況下,如果 1...x-1的位置上存在是它字尾的單詞,所有是它字尾的單詞中,序號最大為 y ,那麼你只要吃 x-y 顆泡椒就能把它記住。
請你幫助 Lweb ,尋找一種最優的填寫單詞方案,使得他記住這 n 個單詞的情況下,吃最少的泡椒。

思路

分析

因為字尾,我們可以想到字典樹,但此題反向建立字典樹。如圖:

其中,橙色的節點表示結尾。

void insert(char *a){
	int ls = strlen(a+1),p = 1;
	for(int i = ls;i >= 1;i--){
		int x = a[i] - 'a';
		if(!trie[p].son[x]) trie[p].son[x] = ++cnt;
		p = trie[p].son[x];
	}
	trie[p].en ++;
}

仔細理解題意後(我居然開始理解錯了) ,我們可以發現:

  1. 對於情況1,我們應該避免,可以按照字典樹從根到子節點遍歷來解決,這樣保證每次背單詞 a 時,它的字尾已經背了。

  2. 情況2意味著這個單詞只有一個字元,是情況3的特殊版本。

因為只有背單詞的順序對答案有影響,因此,我們可以先重構字典樹,把不是單詞結尾的節點刪去。

這樣,每個橙色的節點就代表一個單詞。

vector <int> g[MAXN];
//重建樹
void rebuild(int x){//注意 1 號節點要提前標為單詞節點
	if(trie[x].en && x){
		g[trie[x].las].push_back(x);
		trie[x].las = x;
	}
	for(int i = 0;i < 26;i++) if(trie[x].son[i]){
		int y = trie[x].son[i];
		trie[y].las = trie[x].las;
		rebuild(y);
	}
}

最後一步重要的貪心:

我們發現更新完一個父親節點後,一定要更新完它的子樹,這樣每次更新的代價可以儘可能的小。

但如果有多個子樹呢?

我們要考慮更新子樹順序。

我們先用dfs求出每個子樹大小。

在拿出這個重構的樹來考慮:

我們可以發現,子樹大小更小的子樹應該先更新。

證明

為什麼應該先更新(背)子樹大小更小的子樹?

我們已用圖做出瞭解答,這部分可跳過。

因為一個子樹一定時連著更新的,而更新的順序僅對更新兒子節點代價有影響,因此,決定更新兒子節點代價的是在它前面更新(父親節點後)了多少個節點。

這可以轉化成我們熟悉的打水問題。

n 個人打水,每個人有一個打水時長,在某人打水時,其他未打水的人必須等待,求一種方案使所有人等待時間之和最短。

顯然,此類問題答案肯定是先讓打水時間更短的人打水。

此題中,打水時長即子樹大小,等待時間之和類似於更新這些兒子節點的代價,於是應該先更新子樹大小更小的子樹。

程式碼

#define ll long long
using namespace std;

const int MAXN = 5.1e5+10;
const int MAXS = 5e5+10;

struct Trie{
	int son[26],en,las;
	ll val;
}trie[MAXN];

int n,cnt = 1;
ll ans,num = 0,siz[MAXN];
vector <int> g[MAXN];

// 記得long long

void insert(char *a){ //倒序插入
	int ls = strlen(a+1),p = 1;
	for(int i = ls;i >= 1;i--){
		int x = a[i] - 'a';
		if(!trie[p].son[x]) trie[p].son[x] = ++cnt;
		p = trie[p].son[x];
	}
	trie[p].en ++;
}

void rebuild(int x){//重建樹
	if(trie[x].en && x){
		g[trie[x].las].push_back(x);
		trie[x].las = x;
	}
	for(int i = 0;i < 26;i++) if(trie[x].son[i]){
		int y = trie[x].son[i];
		trie[y].las = trie[x].las;
		rebuild(y);
	}
}

bool cmp (int a,int b){ return siz[a] < siz[b];}

void dfs(int x){//運算元樹大小
	siz[x] = 1;
	for(int i = 0;i < g[x].size();i++){
		dfs(g[x][i]);
		siz[x] += siz[g[x][i]];
	}
	sort(g[x].begin(),g[x].end(),cmp);
}

void getans(int x){//求出次序,算出答案
	ll dfn = num++;
	for(int i = 0;i < g[x].size();i++){
		ans += num - dfn;
		getans(g[x][i]);
	}
	return;
}

int main (){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++){
		char a[MAXS];
		scanf("%s",a+1);
		insert(a);
	}
	trie[1].en = 1; //注意要先標記一下,以免出錯(要在重建樹中用到)
	rebuild(1);
	dfs(1);
	getans(1);
	printf("%lld",ans);
	return 0;
}

Tips: 允許轉載,但請附上原部落格地址:https://www.cnblogs.com/werner-yin/p/solution-P3294.html ,謝謝支援!