[狀壓DP]P1441 砝碼稱重
阿新 • • 發佈:2020-08-06
emm....看到題目,我第一個想到的就是列舉。暴力大法好!
具體怎麼列舉?當然是子集列舉啦!枚舉出每一個可能的砝碼選擇方案。對於每一個合法的(也就是選取數量等於\(n-m\)的)方案,求出這個方案能稱出重量的數量。至於如何求重量的數量,枚舉出這個方案所有的子方案,再對每個子方案的和去重。
這個方法實在是太暴力了
Code:
#include <bits/stdc++.h> using namespace std; #define MAXN 22 #define MAXA 2005 int n,m,a[MAXN],ans,sum[1<<22],cnt[1<<22]; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",a+i); } for(int i=1;i<(1<<n);i++){//狀壓 int high_bit=0,high_bit_num=0; for(int j=31;j>=0;j--){ if((i>>j)&1){ high_bit=1<<j; high_bit_num=j; break; } }//求出當前方案的最高位 sum[i]=sum[i^high_bit]+a[high_bit_num+1];//轉移。因為i是“按順序”列舉的,所以去掉最高位後的方案一定列舉過了。 cnt[i]=cnt[i^high_bit]+1; } for(int i=1;i<(1<<n);i++){ if(cnt[i]==n-m){ set<int> heavy;//set有自動去重的功效 for(int j=1;j<=i;j++){ if((j|i)==i){ heavy.insert(sum[j]);//列舉每個子方案 } } ans=max(ans,int(heavy.size()));//取大 } } printf("%d\n",ans); return 0; }
但是,雖然暴力打得很爽,時間複雜度也非常爆炸。時間複雜度達到了可怕的\(O((2^{n})^2)\)! 所以,優化是必須的。
可以考慮對求重量的數量的過程進行優化。定義狀態\(dp(i)\)代表當前方案能否稱出重量\(i\),\(a(j)\)代表當前考慮的砝碼(當然,這個砝碼必須包含在當前方案裡),容易想出像下面這樣的狀態轉移方程:
\[dp(i)=dp(i-a(1)) \operatorname{OR} dp(i-a(2))...\operatorname{OR} dp(i-a(n)) \]
這個方程翻譯成人話的意思就是:如果一個重量可以被組合出來,那麼再加一個砝碼也能被組合出來。反過來就是,如果一個重量減去一個砝碼的重量能被組合出來,那麼這個重量能夠被組合出來。
有了狀態轉移方程,程式碼就非常好寫了。
#include <bits/stdc++.h> using namespace std; #define MAXN 22 #define MAXA 2005 int n,m,a[MAXN],cnt[1<<22],dp[MAXA],ans; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",a+i); } for(int i=1;i<(1<<n);i++){//狀壓 int high_bit=0,high_bit_num=0; for(int j=31;j>=0;j--){ if((i>>j)&1){ high_bit=1<<j; high_bit_num=j; break; } }//求出當前方案的最高位 cnt[i]=cnt[i^high_bit]+1;//轉移。因為i是“按順序”列舉的,所以去掉最高位後的方案一定列舉過了。 } for(int i=1;i<(1<<n);i++){ if(cnt[i]==n-m){ fill(dp,dp+2000+1,false); dp[0]=true; for(int j=1;j<=n;j++){ if(i>>(j-1)&1){//如果當前砝碼在方案裡才考慮 for(int k=2000;k>=a[j];k--){ dp[k]|=dp[k-a[j]]; } } } int cnt=0; for(int k=1;k<=2000;k++){ if(dp[k]){ cnt++; } } ans=max(ans,cnt); } } printf("%d\n",ans); return 0; }
時間複雜度是\(O(2^n\times n \times \max{a_i})\)仍然非常高,但比之前的不知道低到哪裡去了,足以通過本題。
你都看到這兒了不考慮點一個贊嗎