DP系列之二進制狀態壓縮--杭電1074

狀態壓縮的意圖是用每一位二進制表示一個狀態,0表示選中狀態,1表示不選狀態,如果有N個物體,從中選擇若干個物體,那麼最終選中的狀態可以用一個N位的二進制位來表示

比如

若選擇了第1個物體和第3個物體,這種狀態爲

0...0101 //前面的0的個數爲N-3

若選擇了第2個物體,第3個物體,第N個物體,這種狀態爲

1...0110

因此,無論選中什麼狀態,都可以用一個N位的二進制數來表示,最大值爲(1<<N )- 1 (全部選中)

那麼,如果要記錄每一個狀態的信息(結構體),我們就可以用一個數組來表示,比如,我要記住選中若干物體的重量和價格,只要定義一個結構體

struct Node {

  int weight;

  double price;

};

然後,開闢一個(1<<N )大的數組,data[N],可以預處理將每一種選擇狀態對應的信息都記錄在數組裏面,然後,對於每一次選擇某幾個物品,只需要把對應物品的編號乘以2的冪數然後相加即可在O(1)時間內獲取該選擇策略的信息,這個信息在DP中非常有用,典型的例題便是杭電的1074題,原題見這裏

題目的大致意思是現在某個傢伙有N項作業要做,每項作業有個deadline和costtime,對於其中的每一項作業,如果在deadline之前能夠完成,那麼這個傢伙將平安無事,否則,呵呵,對於每一項作業,完成的時間比deadline晚幾天就扣幾分,給出一系列的homework的名稱和deadline,costtime,要求出扣分最少的方案


開始,我看n比較小,只有1-15這麼點大,果斷暴力全排列,結果一會功夫搞定之後一submit就LTE了,悲劇,後來簡單計算了下時間複雜度,發現是n!,如果n是15的話 15! = 13076,7436,8000 不LTE纔怪了,後來果斷搜索各種解決方案,然後就瞭解到狀態壓縮DP這樣一個東西,於是就有了這篇文章


我們把每一個作業按照字典序列編號(從1開始),然後,每選擇一個作業,就會更新狀態,這裏的狀態指的就是從第一個作業到現在爲止一共耗費了多長時間,扣了多少分,如果用數組來表示的話,我們將要用N維的數組來表示這種狀態的改變,並且這種狀態的改變是在原來的基礎上修改的,因此,我們想到用狀態壓縮,即用一個n(n爲作業數目)位的二進制數來表示每一種狀態,那麼一共有1<<n種狀態,對於每一種狀態,我們要記錄的是最小扣分的信息,利用動態規劃中狀態轉移的思想,每一種狀態(除非是剛開始選的時候)都是由另外一種狀態演變而來,所以,如果要記錄當前狀態的最小扣分信息,我們只要求出當前記錄的所有前導狀態,分別計算以他們爲真正的前導狀態的扣分信息,選擇扣分最小的作爲當前狀態的前導即可,這樣,逐次遞推,直到1<<n-1(由n個1組成),就能計算出所有作業做完的最小扣分數,並且,只要在遞推的時候除了記錄最小扣分信息,我們再加上當前狀態選擇哪個物品以及當前狀態是由哪個狀態演變而來的即可打印除選擇物品的順序

最後源碼如下

#include <iostream>
#include <string>
#define N 15
#define INF 1 << 30
#define MAX 1 << N
using namespace std;

struct Work{
  string name;
  int deadline;
  int costtime;
};

struct State {
  int costtime; //已經花費的時間
  int reduce; //被扣掉的學分
  int pre; //上一個狀態
  int cur;//本次狀態的最後一個作業
};
Work work[N+1];
State state[MAX];

int main() {
  int tot, n, D, C;
  string name;
  cin >> tot;
  while (tot--) {
    cin >> n;
    for (int i = 1; i <= n; i++) {
      cin >> work[i].name >> work[i].deadline >> work[i].costtime;
    }
    int max_value = (1 << n);
    state[0].costtime = 0;
    state[0].reduce = 0;
    state[0].pre = -1;
    state[0].cur = 0;

    for (int i = 1; i < max_value; ++i) {
      state[i].reduce = INF;//當前狀態的最小扣分設置爲無窮大

      for (int j = n; j >= 1; j--) {//每一個j都表示一個當前狀態最後做的作業,由於題目需要按照字典序列輸出,因此這裏要逆序,即從字母序列最大的開始枚舉,比如當我們求到最後一個狀態的時候, 從 (......)->j 可以看出j當然越大越好,因爲這樣越大的可以儘量排在後面,假如先計算出最後一個選擇n和最後一個選擇n-1的情況下最小扣分數相同,由於"//*****"是嚴格小於,因此並不會將n-1換掉n,因此保證了字典序列
        int tmp = (1 << (j-1));
        //如果狀態i本次完成第j項作業
        if (i&tmp) {
          int pre = i - tmp;//這裏計算出的pre爲i的前導狀態
          int reduce = state[pre].reduce;//表示在前導狀態pre下完成j之後的扣的分數
          if (state[pre].costtime + work[j].costtime > work[j].deadline) {
            reduce += state[pre].costtime + work[j].costtime - work[j].deadline;
          }

          if (reduce < state[i].reduce) { //*****
            state[i].costtime = state[pre].costtime + work[j].costtime;
            state[i].reduce = reduce;
            state[i].pre = pre;
            state[i].cur = j;
          }
        }
      }
    }
      printf("%d\n", state[max_value-1].reduce);

      int cur = max_value - 1;
      int *pos = new int[n+1];
      int k = n;
      while (state[cur].pre != -1) {
        int now = state[cur].cur;
        pos[k--] = now;
        cur = state[cur].pre;
      }
      for (int i = 1; i <= n;++i)
        cout << work[pos[i]].name << endl;


  }
  return 0;
}

注:鄙人最近按照杭電ACM分類來刷題,假期的最低限度是刷掉所有的DP類,並且每一道題目寫一個解題報告,如果有志同道合的朋友,歡迎加QQ 823797837共同學習交流,也可以加羣ACM新手羣161986576,老鳥飛過


發佈了140 篇原創文章 · 獲贊 70 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章