本人在準備藍橋杯的過程中,刷到了這麼一道題,是2019年藍橋杯省賽Java A組的一道題,題目如下:
我本來打算來一手貪心,萬萬沒想到,恕我才疏學淺,此題的主流解法使用狀態壓縮動態規劃(狀壓dp)。無奈,百度之,學習。
總結下來各位博主的博客,用狀壓dp做題的突出特徵,就是此題的空間規模,有着特殊“小”的特點,比如上面這道糖果題,N,M,K都可以設置的很大,用來對我們的代碼做壓力測試,但是反而,範圍都很小,常規思路來看的話,沒有壓力測試。
這裏給出這位博主的代碼,對結構稍加修改並加以鄙人的註釋。
原文:2019第十屆藍橋杯省賽JavaA組題解
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n=sc.nextInt();
int m=sc.nextInt();
int k=sc.nextInt();
int[][] a = new int[n][k];
int[] sta = new int[n];
int[] dp = new int[1<<k];
Arrays.fill(dp, -1);
dp[0]=0; // 動規數組只有dp[0]爲0,其餘全部初始化-1
for(int i=1; i<=n; i++){
for(int j=1; j<=k; j++){
a[i][j] = sc.nextInt(); // 讀取數據
/* 此處用sta[i]做了k次或賦值,解釋如下。 */
/* 對於數據a[i][j],意味第i個包裹中的第j塊糖果的種類 */
/* 比如a[i][j]是第4類糖果,那麼1<<(a[i][j]-1)的值爲二進制的1000,也就是第四類糖果被選中。 */
/* 當sta[i]做了k次或賦值後,sta[i]的二進制上會有若干個1,表示第i個包裹中有哪些類別的糖果。 */
sta[i] |= 1 << (a[i][j] - 1);
}
/* 這裏說明一下dp數組的含義 */
/* dp[l] = k,表示要想獲得l的二進制表示的所有糖果種類,至少要選取k個包裹。 */
/* 由於我們剛剛初始化sta數組,那麼這裏表示的含義就是對於每一個包裹,要想獲得這個包裹裏的所有糖果種類,需要1個包裹。 */
dp[sta[i]] = 1;
}
/* 接下來就是動規得出答案了 */
for(int i=1; i<=n; i++) { // 依次考慮每一個包裹,獲取包裹後會對dp有什麼影響
for(int j=0; j<(1<<m); j++) { // 在考慮包裹i時,對所有已經存在的狀態j都做分析,看包裹i是否對狀態j有所影響
if (dp[j] == -1) continue; // 狀態j不存在,考慮下一個
/* 分兩種情況 */
/* 狀態j存在,但拿到包裹i後,狀態j|sta[i]不存在,更新之 */
/* 狀態j存在,拿到包裹i後的狀態也存在,但在狀態j下拿到包裹i,比之前的代價更小,更新之 */
if (dp[j | sta[i]] == -1 || dp[j] + 1 < dp[j|sta[i]]) {
dp[j | sta[i]] = dp[j] + 1;
}
}
}
System.out.println(dp[(1 << m) - 1]); // 最後輸出的是要拿到所有糖果種類需要的最少包裹數
}
還有一道題是經典的棋盤覆蓋問題,此題在很多狀壓的博客上都出現過,只是看來大家學算法用的都是c++,而我用的是Java,費了半天勁看懂了各位的代碼,改成Java語言,放在這裏。
原文在這:NYOJ 515 完全覆蓋 II (狀態壓縮dp)
static int[][] dp;
static int n;
static int m;
public static int main() {
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
if (n % 2 == 1 && m % 2 == 1) { // 如果長和寬都爲奇數,則方案數爲0
System.out.println(0);
}
if (n < m) { // 爲了減少情況數量,使小的爲列數
int temp = n;
n = m;
m = temp;
}
dp = new int[n + 1][1 << m];
solve();
System.out.println(dp[n][1 << m]);
}
void solve(){
int s,ss,i;
int maxState=(1 << m) - 1;
for (int s = 0; s <= maxState; s++){ // 第一行每一種可行的情況
if (judge1(s)) {
dp[1][s] = 1;
}
}
for (int i = 2; i <= n; i++) { // 從第二行開始
for (int i_1s = 0; i_1s <= maxState; i_1s++) { // 考慮上一行的每一個狀態
for (int i_s = 0; i_s <= maxState; i_s++) { // 是否第i行可以行上一行兼容
if (judge2(i_1s, i_s)) { // 判斷第i-1行與第i行情況是否兼容
dp[i][i_s] += dp[i-1][i_1s]; // 累加
}
}
}
}
}
public static boolean judge1(int s){ // 判斷的標準是必須連續兩格爲1
for (int i=0; i<m; ) {
if (s & (1<<i)) { //如果s的第i位爲1
if (i == m-1) return false; // 如果s的最後一位爲1,則判斷爲假
else if (s & (1<<(i+1))) i += 2; // 如果不是最後一位的爲1,且下一位也爲1,則i跳2位
else return false; // 否則位假
}
else i++; // s的第i位爲0,略過
}
return true; // 返回真
}
public static boolean judge2(int s, int ss){//判斷第i-1行的s情況與i行的情況是否兼容
// s爲上一行,ss爲當前行
for (int i=0; i<m; ) {
if (!(s & (1<<i))) { // s的第i位爲0
// 下面這個if是對當前行的判斷,邏輯與judges1相同
if (ss & (1<<i)) { // ss的第i位也爲1
if(i == m-1 || !(s &(1<<i+1)) || !(ss &(1<<i+1)))
// 如果i是最後一位,或s的下一位爲0,或ss的下一位爲0,則返回假,這裏的判斷邏輯和judge1相同
return false;
else
i += 2;
}
else // ss的第i位爲0,略過
i++;
} else { // s的第i位爲1
if (!(ss & (1<<i))) // 那麼ss的第i位爲0時,則沒問題
i++;
else // ss的第i位爲1時,就爲假
return false;
}
}
return true;
}
原文的代碼被我改了邏輯,judge2的判斷條件被我給改了,我覺得原文有誤。我的理解是,對於狀態的某一位,如果上一行爲1,則下一行必須爲0,如果上一行爲0,則對下一行做橫向判斷。也就是說,某一位爲0,表示此處沒有覆蓋,或與上一行的1形成2*1覆蓋。某一位爲1,表示此處有覆蓋,但僅限於橫向覆蓋或與下一行的0形成縱向覆蓋。
上面給的代碼正確性有待考證,歡迎各位指正。