狀態壓縮動態規劃(Java)

本人在準備藍橋杯的過程中,刷到了這麼一道題,是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形成縱向覆蓋

上面給的代碼正確性有待考證,歡迎各位指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章