P5911 [POI2004]PRZ (狀態壓縮dp+列舉子集)
阿新 • • 發佈:2020-08-05
題目背景
一隻隊伍在爬山時碰到了雪崩,他們在逃跑時遇到了一座橋,他們要儘快的過橋。
題目描述
橋已經很舊了, 所以它不能承受太重的東西。任何時候隊伍在橋上的人都不能超過一定的限制。 所以這隻隊伍過橋時只能分批過,當一組全部過去時,下一組才能接著過。隊伍裡每個人過橋都需要特定的時間,
當一批隊員過橋時時間應該算走得最慢的那一個,每個人也有特定的重量,我們想知道如何分批過橋能使總時間最少。
輸入格式
第一行兩個數: W 表示橋能承受的最大重量和 n 表示隊員總數。
接下來 n 行:每行兩個數: t 表示該隊員過橋所需時間和 w 表示該隊員的重量。
輸出格式
輸出一個數表示最少的過橋時間。
輸入輸出樣例
輸入 #1
100 3
24 60
10 40
18 50
輸出 #1
42
說明/提示
對於 100% 的資料,100≤W≤400,1≤n≤16,1≤t≤50,10≤w≤100。
前置芝士
-
列舉子集
首先,我們先看一下列舉子集是什麼東西。
在狀態壓縮dp時,我們一般的套路就是列舉兩個狀態\(i\) 和 \(j\),判斷 \(j\) 是否是 \(i\) 的子集,這樣來說複雜度時O(4^n)
但,根據二項式定理,一個集合的子集最多有3^n 嚴格列舉的話,可以將複雜度變為O(3^n)
程式碼
對於這道題,n的範圍很小,我們可以考慮對n進行狀態壓縮
f[i] 表示達到 \(i\) 這個狀態所需要的最小時間 \(i\)
轉移的話,我們可以列舉\(i\)的子集,就是考慮這次有哪些人乘船
f[i] = min(f[i],f[i-j] + tim[j]);//i是我們想達到的狀態,j是這次要運的狀態,i-j是沒運j這次之前的狀態
對於,每個狀態所花費的時間,我們可以在之前就預處理出來。
當然,你也可以在列舉j的時候算,只是這樣你多算了很多狀態,你就會穩穩的TLE
看不懂的童鞋,下面程式碼有註釋。
程式碼
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int n,m,t[20],w[20],base[20],f[65540],tim[65540],maxw[65540]; inline int read() { int s = 0, w = 1; char ch = getchar(); while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();} while(ch >= '0' && ch <= '9'){s = s * 10+ch -'0'; ch = getchar();} return s * w; } int main() { m = read(); n = read(); for(int i = 0; i <= n-1; i++) { t[i] = read(); w[i] = read(); } base[0] = 1; for(int i = 1; i <= n; i++) base[i] = base[i-1] * 2;//處理一下2的進位制 for(int i = 0; i < base[n]; i++)//列舉每個狀態 { for(int j = 0; j < n; j++)//列舉每個人 { if((i & (1<<j)) == 0)//判斷這個人在i這個狀態是否已經乘船,沒乘船的話,可以轉移得到下一個狀態 { tim[i | (1<<j)] = max(tim[i],t[j]);//i|(1<<j)即把i的第j位賦1,就像於第j個人坐了船後,i所變成的狀態 maxw[i |(1<<j)] = maxw[i] + w[j]; //進行轉移 } } } for(int i = 0; i < base[n]; i++) f[i] = 2333333;//初始化為無窮大 f[0] = 0; for(int i = 1; i < base[n]; i++)//列舉每個狀態 { for(int j = i; j; j = (j-1) & i)//列舉子集 { if(maxw[j] <= m) f[i] = min(f[i],f[i-j] + tim[j]);//j你可以理解為這次要運的狀態,i-j就是i沒運j之前i的狀態 } } printf("%d\n",f[base[n]-1]); return 0; }