1. 程式人生 > 其它 >P7519-[省選聯考 2021 A/B 卷]滾榜【狀壓dp】

P7519-[省選聯考 2021 A/B 卷]滾榜【狀壓dp】

正題

題目連結:https://www.luogu.com.cn/problem/P7519


題目大意

\(n\)個隊伍,隊伍之間按照得分從小到大排名,得分相同的按照編號從小到大排。開始時每個隊伍有個初始得分\(a_i\),和一個額外分\(b_i\),主持人會按照\(b_i\)不降的順序讓每個隊伍的得分加上\(b_i\),並且要求每次加上後這個隊伍都變成第一名。

已知每個隊伍的初始分\(a\)和額外分的和\(m\),求有多少種公佈額外分的序列。

\(1\leq n\leq 13,1\leq m\leq 500,0\leq a_i\leq 10^4\)


解題思路

顯然地一點是我們考慮一個序列合法時\(b_i\)

的和的最小值,然後和\(m\)進行比較

開始思路時卡了,假設已經排好了一部分,我們需要把一個新的排在後面,此時會有兩個限制:

  1. 這個隊伍的得分\(a_i+b_i\)必須是已經公佈的裡面最大的(或相同的序號最小的)
  2. \(b_i\)必須是已經公佈的裡面最大的。

顯然記錄上這兩個限制進行的\(dp\)並不方便,考慮如何去掉一個限制,因為\(b_i\)是我們可以任意調整的,並且要求遞增,可以每次操作都讓後面所有數字的\(b_i\)都同時加值,這樣就不需要考慮第二個限制了。

然後是第一個限制如何處理,注意到我們在剛剛的情況下,假設限制最後公佈的一個是\(i\),而下一個公佈的是\(j\),那麼此時有\(b_j=b_i\)

,所以此時兩個數加上\(b\)之後的差值不變,所以直接拿\(a_i-a_j\)就可以知道後面的\(b_j\)要加上多少了。

那麼做法已經顯然了,設\(f_{s,i,j}\)表示現在已經插入的數狀態為\(s\)\(b_i\)的和為\(i\),目前最後一個公佈的是\(j\),然後\(O(n)\)轉移即可。

時間複雜度:\(O(2^nmn^2)\),實際上很多狀態是不會到達的,所以能過。


code

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll N=13;
ll n,m,ans,a[N],c[1<<N],f[1<<N][501][N];
signed main()
{
	scanf("%lld%lld",&n,&m);
	ll pos=0;
	for(ll i=0;i<n;i++){
		scanf("%lld",&a[i]);
		if(a[i]>a[pos])pos=i;
	}
	f[0][0][pos]=1;
	ll MS=(1<<n);
	for(ll s=1;s<MS;s++)c[s]=c[s-(s&-s)]+1;
	for(ll s=0;s<MS;s++)
		for(ll k=0;k<=m;k++)
			for(ll i=0;i<n;i++){
				if(!f[s][k][i])continue;
				for(ll j=0;j<n;j++){
					if((s>>j)&1)continue;
					ll w=max(a[i]-a[j]+(j>i),0ll)*(n-c[s]);
					if(k+w>m)continue;
					f[s^(1<<j)][k+w][j]+=f[s][k][i];
				}
			}
	for(ll i=0;i<=m;i++)
		for(ll j=0;j<n;j++)
			ans+=f[MS-1][i][j];
	printf("%lld\n",ans);
	return 0;
}