1. 程式人生 > 實用技巧 >[狀壓DP]P1441 砝碼稱重

[狀壓DP]P1441 砝碼稱重

前置知識:狀壓DP

洛谷傳送門

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})\)仍然非常高,但比之前的不知道低到哪裡去了,足以通過本題。

你都看到這兒了不考慮點一個贊嗎