【2018/10/05】T2
我好菜啊……好菜啊……好菜啊……
上升序列
描述
給出一個長度為 m 的上升序列 A(1 ≤ A[i]≤ n), 請你求出有多少種 1...n 的排列, 滿足 A 是它的一個 LIS.
輸入
第一行兩個整數 n,m.
接下來一行 m 個整數, 表示 A.
輸出
一行一個整數表示答案.
【輸入樣例1】 5 3 1 3 4 【輸出樣例1】 11 【輸入樣例2】 4 2 3 4 【輸出樣例2】 5
【資料範圍與約定】
對於前 30% 的資料, n ≤ 9;
對於前 60% 的資料, n ≤ 12;
對於 100% 的資料, 1 ≤ m ≤ n ≤ 15.
分析
這個資料範圍很狀壓,但我依舊沒有看出來,為什麼那麼弱啊%>_<%(我好菜啊……好菜啊……好菜啊……)
但即使知道是狀壓,也沒搞出來
後面在ldx大佬的耐心講解下,總算是會了,%%%%%%%%廣告%%%%%%%%%
先知道最長不降子序列的一種求法是(就是那個優秀演算法 nlogn):使用附加陣列D[i],表示當前長度為 i 的最長不降子序列最小的結尾是多少,顯然它是單調遞增的。每插入一個數就二分找到第一個大於它的位置替換。
我們可以設狀態S1,第i位為 1表示它在D 中出現了,出現的位置就是1~i位 1 的數量 現在就可以設遞推狀態 f (S,S1) 表示當前用了S中的數(就是1~n),D的狀態為S1的方案數 轉移很容易轉移,列舉這一位選什麼直接更新
但如果簡單這樣搞的話,時間和空間限制都會受不了啊,因此我們再觀察一下,注意到S1一定是S的子集,因為要在D中出現,那麼這個數肯定被 S 選了,於是可以用三進製表示
然後我就懵了,什麼三進製表示????
好吧,其實就是這樣的,
0表示這一位的數既沒有在S中出現,又沒有在S1中出現
1表示在S中出現了或者在S1中出現了
2表示既在S中又在S1中
然後就亂搞了……看程式碼吧,雖然ldx大佬diss我誰還會加註釋啊,但我依然要加,╭(╯^╰)╮
程式碼
#include <bits/stdc++.h> #define ll long long//注意一下範圍咯 using namespace std; int n,m,a[20],s1[20],s2[20]; ll f[14348990],ans=0;//f陣列的大小 3^15 bool check(int sta){//a陣列的數必須是有順序有先後關係的出現 int flag=0; for(int i=1;i<=m;++i){ if((sta&s1[a[i]-1])==0) flag=1; else if(flag) return false ; } return true; } void dfs(int pos,int use,int sta){//統計答案,選了n個數,且LIS的長度為m if(pos==n+1){if(!use) ans+=f[sta];return;} sta+=s2[pos-1];dfs(pos+1,use,sta); sta+=s2[pos-1];dfs(pos+1,use-1,sta); } int main(){ int i,j,k; scanf("%d%d",&n,&m); for(i=1;i<=m;++i) scanf("%d",&a[i]); s1[0]=s2[0]=1; for(i=1;i<=15;++i) s1[i]=s1[i-1]*2,s2[i]=s2[i-1]*3; f[0]=1;//dp問題的初始值 for(i=0;i<s1[n];++i){//列舉S的所有狀態 if(!check(i)) continue; int sub=i; for(int sub=i;1;sub=(sub-1)&i){//列舉S1,因為S1是S的子集嘛 //這個列舉方法一定要學到 int sta=0;//現在就根據S和S1推出三進位制的表示 for(int j=1;j<=n;++j){ if(i&s1[j-1]) sta+=s2[j-1]; if(sub&s1[j-1]) sta+=s2[j-1]; } if(f[sta]){//往下轉移 for(int j=1;j<=n;++j){ int msta=sta; if(msta/s2[j-1]%3==0){//如果這個數沒有被使用過 msta+=2*s2[j-1]; for(int k=j+1;k<=n;++k) if(msta/s2[k-1]%3==2) {msta-=s2[k-1];break; }//更新d陣列,不清楚的同學可以參見我之前的部落格 f[msta]+=f[sta];//累計 } } } if(!sub) break; } } dfs(1,m,0); cout<<ans; return 0; }