1. 程式人生 > 其它 >【題解】P3694 邦邦的大合唱站隊

【題解】P3694 邦邦的大合唱站隊

【題解】P3694 邦邦的大合唱站隊

題目傳送門

題目描述

N個偶像排成一列,他們來自M個不同的樂隊。每個團隊至少有一個偶像。

現在要求重新安排佇列,使來自同一樂隊的偶像連續的站在一起。重新安排的辦法是,讓若干偶像出列(剩下的偶像不動),然後讓出列的偶像一個個歸隊到原來的空位,歸隊的位置任意。

請問最少讓多少偶像出列?

Solution

首先,想象最終佇列的排列方式,其最多隻有M!種

如111222233,或222233111,331112222……

於是我們可以用每一種排列方式分別與原數列的每一個位置依次進行比對,

顯然,若原數列的相同位置上的數與最終數列上的數相同,則其一定不用出列

否則,則一定要出列

O(N)統計個數即可,總複雜度O(NM!)

考慮優化統計的複雜度

可以用字首和sum[i][j]記錄原數列前i個位置,共有多少個偶像來自團隊j
再用一個num[i]表示團隊i的總個數
這樣每次統計O(M),總複雜度O(M*M!)

如何用狀壓dp呢

用s表示,當前哪些隊伍已被排完。第i位是1,就表示第i個團隊被排完

用f[s]表示前count(s)個隊伍有哪些,排好他們的最少花費

轉移時,列舉每一個可能的結尾隊伍,按照剛才的方法算出花費,記錄最小值

複雜度是(m*2^m)

那麼為什麼複雜度降低了呢

回憶我們一開始的全排列解法

比如1234和2134兩個排列,他們的前兩個數是相同的,只是順序不一樣,而在全排列解法中,我們卻重複算了兩遍

但是!!!實際上,在轉移時我們根本不關心他前面具體的順序,我們只需要知道他已經排了哪幾個團隊,以及這些團隊的佔用的總長度,而後者可以用前者計算

dp之所以比暴力快,就在於它及時統計狀態資訊,避免重複運算

所以我們可以想到以當前排好了哪幾個隊伍為狀態,那麼也很自然的就運用到了狀態壓縮

總結

  1. 有些題沒有思路,可以採取想象最終答案,並與原資料進行比對的策略
  2. 在從暴力優化到正解的過程中,可以考慮哪些資訊被重複統計,是否狀態冗餘,然後想辦法壓縮狀態,或記錄資訊

Code

#include<bits/stdc++.h>
using namespace std;

inline int read()
{
	register int x=0,w=1;
	register char ch=getchar();
	while((ch<'0'||ch>'9')&&ch!='-') ch=getchar();
	if(ch=='-') {ch=getchar();w=-1;}
	while(ch>='0'&&ch<='9') {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();	}
	return x*w;
}
const int M=1e5+10;
int n,m,f[1<<20],sum[M][21],tot[21],len[1<<20];
int main()
{
    n=read();m=read();
    int x;
    for(int i=1;i<=n;++i)
    {
    	x=read();
    	for(int j=1;j<=m;++j) sum[i][j]=sum[i-1][j];
    	sum[i][x]++;
    	tot[x]++;
	}
	memset(f,0x3f,sizeof(f));
	f[0]=0;
	for(int i=1;i<(1<<m);++i)
	{
		for(int j=0;j<m;++j)
		{
			if(i>>j&1) len[i]+=tot[j+1];
		}
	}
	for(int i=1;i<(1<<m);++i)
	{
		for(int j=0;j<m;++j)
		{
			if(i>>j&1) f[i]=min(f[i],f[i^(1<<j)]+tot[j+1]-(sum[len[i]][j+1]-sum[len[i^(1<<j)]][j+1]) );
		}
	}
	cout<<f[(1<<m)-1];
	return 0;
}