醬神尋寶 狀壓DP
醬神來到了一座小島,島上有nn個箱子。
一共有33中不同的鑰匙,金鑰匙、銀鑰匙和萬能鑰匙。醬神一開始有aa把金鑰匙、bb把銀鑰匙和cc把萬能鑰匙。
第ii個箱子上有xixi把金鎖,yiyi把銀鎖。金鑰匙只能開啟金鎖,銀鑰匙只能開啟銀鎖,萬能鑰匙兩種鎖都能開啟。用於開啟鎖的鑰匙會立刻損壞,醬神會丟掉損壞的鑰匙。箱子裡有aiai把金鑰匙、bibi把銀鑰匙和cici把萬能鑰匙,想要取出箱內的鑰匙必須要開啟這xi+yixi+yi把鎖。
醬神的目的是使他擁有的鑰匙總數最多。一旦醬神認為自己已經擁有了最多的鑰匙,他就不會去開剩下的箱子了。
Input
第一行一個數nn。
接下來有nn行。每行55個數,xi,yi,ai,bi,cixi,yi,ai,bi,ci。
最後一行3個數a,b,ca,b,c。
1=<n<=151=<n<=15
0=<xi,yi,ai,bi,ci,a,b,c<=100=<xi,yi,ai,bi,ci,a,b,c<=10
Output
輸出一個數醬神的最多鑰匙數。
Sample Input
3 1 0 0 0 1 2 4 0 8 0 3 9 10 9 8 3 1 2
1 0 0 1 2 3 0 0 0
Sample Output
8
6
Hint
第一個樣例中醬神會開啟第一個和第二個箱子。
很不幸,這道題好像交不了了。
但是,我還是看了一天的這道題,終於搞懂了一點,下面是我自己拼的思路和程式碼,都是別人的,但是我覺得是網上最好了的。
題意:
醬神來到了一座小島,島上有n個箱子。
一共有3中不同的鑰匙,金鑰匙、銀鑰匙和萬能鑰匙。醬神一開始有a把金鑰匙、b把銀鑰匙和c把萬能鑰匙。
第i個箱子上有xi把金鎖,yi把銀鎖。金鑰匙只能開啟金鎖,銀鑰匙只能開啟銀鎖,萬能鑰匙兩種鎖都能開啟。用於開啟鎖的鑰匙會立刻損壞,醬神會丟掉損壞的鑰匙。箱子裡有ai把金鑰匙、bi把銀鑰匙和ci把萬能鑰匙,想要取出箱內的鑰匙必須要開啟這xi+yi把鎖。
醬神的目的是使他擁有的鑰匙總數最多。一旦醬神認為自己已經擁有了最多的鑰匙,他就不會去開剩下的箱子了。
思路:
金鑰匙和銀鑰匙都具有指定性,只能開啟特定的鎖。所以在相同的情況下,我們儘可能地保留萬能鑰匙(貪心策略)。
則將這個策略運用於DP,我們定義狀態為 dp[st][j] 。st是一個二進位制數,利用狀壓儲存了箱子的狀態,1表示該位對應的箱子已經開啟,0表示還沒有開啟。j儲存的是當前醬神持有的金鑰匙的數目。dp儲存了當前醬神持有的最多的萬能鑰匙的數目。而銀鑰匙可以通過金鑰匙數目、萬能鑰匙數目和st的狀態計算得到。
每次狀態轉移時,從st中列舉當前為零的位數(寶箱編號),依次置一,從而得到新狀態。
下面開始添油加醋。
對於不會做的同學,看到這個題解肯定是滿臉懵逼,so do I, 但是我們至少知道了一點,dp存的是啥,知道這個再去想想,可能有點思路,但是我還是沒想出來。
我們先想想,如果直接暴力所有情況,就要n!,n的全排列種情況,肯定會超時,但是這些情況中,很多都是重複的,所以我們就可以優化一下,先有一個 表示不帶開箱子順序的列舉,這個怎麼來的呢,很簡單,有n個箱子,每個有0,1兩種狀態,每次開一些箱子與不開,都會形成一種狀態,但是這是不講順序的,實際上還可能存在重複的,因此我們再加一個狀態,表示醬神有的金鑰匙數目,這樣我們就可以唯一確定一種情況了。
然後演算法的步驟是:
列舉1- 的所有狀態,對於每一種狀態,如果有一位是1,則再考慮這一位不是1的前一個狀態,即本來這個箱子是要開的,但是這個狀態可以從前面推導,從不開這個箱子的推導,然後再考慮推導後的狀態是否可行(鑰匙是否夠)。
下面是我找到AC的一份程式碼,自己加了詳細註釋
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <string>
#include <iostream>
#include <algorithm>
#include <stack>
#include <queue>
#include <vector>
#include <deque>
#include <set>
#include <map>
#define fori(l,r) for( int i = l ; i <= r ; i++ )
#define mem(a,val) memset(a,val,sizeof a)
#define mid (l+r)>>1
#define lef rt<<1
#define rig rt<<1|1
using namespace std;
#define inf 0x3f3f3f3f
typedef long long ll;
int n;
struct Box
{
int need_x,need_y; //需要的金、銀
int newa,newb,newc; //箱子裡面的鑰匙
};
const int maxn = 1<<15+10;
Box box[20];
int num[maxn];
map<int,int> dp[maxn]; //相當於二維陣列
//和dp[i][j] 本質上一樣 ,表示在i的狀態下,當前有j把金鑰匙,dp裡面儲存萬能鑰匙數
int main()
{
scanf("%d",&n);
//while( scanf("%d",&n) == 1 )
{
for( int i = 0 ; i < (1<<n) ; i++ )
{
dp[i].clear();
num[i] = 0;
}
fori(0,n-1)
{
scanf("%d %d %d %d %d",&box[i].need_x,&box[i].need_y,&box[i].newa,&box[i].newb,&box[i].newc);
}
int a,b,c; //一開始的鑰匙
scanf("%d %d %d",&a,&b,&c);
dp[0][a] = c;
int r = 1<<n;
map<int,int>::iterator it;
int ans = c;
num[0] = a+b+c;
for( int i = 1 ; i < r ; i++ )
{
num[i] = num[0];
for( int j = 0 ; j < n ; j++ ) //判斷每一位是否為1
{
int t = 1<<j;
if( t&i )
{
num[i] += box[j].newa+box[j].newb+box[j].newc-box[j].need_x-box[j].need_y;
int pre_state = i^t; //不放第j個物品的狀態
for( it = dp[pre_state].begin() ; it != dp[pre_state].end() ; it++ )
{
a = it->first; //上一個狀態的金鑰匙數
c = it->second; //上一個狀態的萬能鑰匙數
b = num[pre_state]-a-c; //上一個狀態的銀鑰匙數
a -= box[j].need_x; //因為開這個箱子,所以需要消耗鑰匙
b -= box[j].need_y; //由此更新新的狀態
if( a+c >= 0 && b+c >= 0 && num[pre_state] >= box[j].need_x+box[j].need_y ) //如果是可以開這個箱子
{
if( a < 0 ) //可能減成負,由萬能鑰匙來替代
{
c += a;
a = 0;
}
if( b < 0 )
{
c += b;
b = 0;
}
a += box[j].newa; //得到箱子裡面的鑰匙
b += box[j].newb;
c += box[j].newc;
if( dp[i].find(a) != dp[i].end() )//這個地方,我覺得應該是最大max,但是別人的程式碼是min,我也無法驗證
dp[i][a] = min(dp[i][a],c); //程式碼提交不了,沒辦法
else dp[i][a] = c; //相當於新的,插入一個新的
}
}
}
}
if( dp[i].size() )
ans = max(ans,num[i]);
}
printf("%d\n",ans);
}
return 0;
}
/*
100
6
1 6 8 9 9 8
*/