SCOI2007 排列
傳送門
這道題竟然可以使用全排列暴力模擬水過……
不過我們還是說一下正解。既然數據範圍這麽小,所以我們考慮狀壓DP。
用dp[i][j]表示狀態為i時,當前選取的所有數的排列,其對d取模後結果為j有多少種情況。其中i是一個二進制數字串,每一個二進制位對應原數組中的數字有沒有被選中。
簡單的解釋一下,假設原數組中是1246,那麽當狀態為0011時,我們相當於求4,6這兩個數字組成的全排列,對d取模後結果為j有多少種情況。
DP方程如下,若(i & 1<<k) == 0 ,則dp[i | (1 << k)][(j * 10 + f[k]) % d] += dp[i][j]
這裏解釋一下,其實就相當於我們在每次dp轉移的時候又取了一個數,並且把取得這個數加到末尾,計算一共有多少種排列對d取模之後結果為j。
比如說(原數組還是用上面的),從狀態1010轉移至1011就相當於是把1,4的全排列末尾加上6,之後計算。
有人可能會有疑問,你要算的是當前選取的所有數(1,4,6)的全排列可能產生的方案數,而你當前只計算了6在末尾的情況,它在中間的情況你並沒有計算。
其實並不是這樣。對於狀態1011,它可以從不止一個狀態中轉移過來。比如0011,1010,1001.而這三種狀態其實恰好就對應了排列1,4,排列4,6,排列1,6,把新加入的數分別放在他們的後面,當前選取的三個數的全排列還是會被完全考慮到的。
同理,對於任意一種已經被轉移的狀態,其必然已經計算過當前選取的所有數字的全排列的情況,所以也就必然能保證所有情況都被枚舉到。
再說的通俗一點,拿上面的舉例。比如狀態1010,他可以由1000和0010轉移,所以狀態1010必然已經包含過前面兩種狀態構成的所有情況,當他繼續向後DP的時候亦然。
還有一點比較顯然,我們直接把上一次取模的結果*10加上當前數再取模即可,因為他和原數肯定是同余的。
這樣dp方程的正確性就很顯然了。
註意應該怎麽dp,初始值dp[0][0] = 1.然後註意dp的時候要從0開始,不要取錯。
還有就是遇到了一個有重復元素的情況。比如說122.122被轉移的時候,可以從狀態110,101,011轉移過來。而前兩種狀態計算的是重復的。再類推一下,可以得到,每個重復的元素會貢獻其出現次數的階乘倍的多余答案,應該被除去。
這樣就可以了。
#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<queue> #include<cstring> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar(‘\n‘) using namespace std; typedef long long ll; const int M = 15; ll n,L,f[M],dp[2000][2000],num[M],t,d,len,sum,cur,ans; ll read() { ll ans = 0,op = 1; char ch = getchar(); while(ch < ‘0‘ || ch > ‘9‘) { if(ch == ‘-‘) op = -1; ch = getchar(); } while(ch >=‘0‘ && ch <= ‘9‘) { ans *= 10; ans += ch - ‘0‘; ch = getchar(); } return ans * op; } char s[M]; int main() { t = read(); while(t--) { memset(dp,0,sizeof(dp)); memset(f,0,sizeof(f)); memset(s,0,sizeof(s)); memset(num,0,sizeof(num)); scanf("%s",s+1); len = strlen(s+1); rep(i,1,len) f[i] = s[i] - ‘0‘,num[f[i]]++; d = read(); dp[0][0] = 1; rep(i,0,(1<<len)-1) rep(j,0,d-1) rep(k,0,len-1) if(!(i & (1<<k))) dp[i|(1<<k)][(j*10+f[k+1])%d] += dp[i][j]; ans = dp[(1<<len)-1][0]; rep(i,0,9) while(num[i] > 1) ans /= num[i],num[i]--; printf("%lld\n",ans); } return 0; }
SCOI2007 排列