狀態壓縮DP(以及例題)

狀壓DP

——一種美妙的動態規劃

在解這道有省選難度的題目之前
先給大家介紹一道經典狀態壓縮DP例題:

吃奶酪

房間裏放着n塊奶酪。一隻小老鼠要把它們都吃掉,問至少要跑多少距離?老鼠一開始在(0,0)點處。

輸入輸出格式
輸入格式:
第一行一個數n (n<=15)

接下來每行2個實數,表示第i塊奶酪的座標。

兩點之間的距離公式=sqrt((x1-x2)(x1-x2)+(y1-y2)(y1-y2))

輸出格式:
一個數,表示要跑的最少距離,保留2位小數。

這道題這一眼看是dfs,通過普通剪枝就能過,(洛谷數據水),如果這個點數上升到19的話,就必修通過狀壓DP了,
我們用jj<=2n+1 表示狀態,即當前哪些點已經走過了,哪些點還沒有走過,首先我們要滿足的是,我們要進行狀態轉移的這個點必須是我們還沒有走過的點,也就是滿足j &2i==0 ,就是在j的二進制上,i的這一位必須是0。
然後狀態轉移方程是
f[i][j]=min(f[i][j],f[k][j(1<<i)]+dist(a[i],a[k]));
f[i][j] 表示前i 個點在j 的狀態下的最優解
那麼我們保證我們每次循環的點,i,k 是我們枚舉的點,k 的狀態由i 的狀態轉移而來,也就是,到這一步,不經過i 經過k 的最優狀態,那麼就好了。.
因爲我們先枚舉的是所有的狀態,然後不經過i 了,選擇經過k ,這樣的話在時間複雜度不大,代碼量又小的情況下達到了最優解。

#include<bits/stdc++.h>
using namespace std;
#define INF 1000000000
struct node
{
    double x,y;
}a[20];
int n;
double f[20][100000];
double dist(node a,node b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
int main()
{
    cin>>n;
    for (int i=1;i<=n;i++) cin>>a[i].x>>a[i].y;
    for (int i=0;i<=n;i++)
    for (int j=0;j<(1<<(n+1));j++) f[i][j]=INF;
    for (int i=0;i<=n;i++) f[i][1<<i]=0;
    for (int j=0;j<(1<<(n+1));j++)
        for (int i=0;i<=n;i++)
        {
            if ((j&(1<<i))==0) continue;
            for (int k=0;k<=n;k++)
                if (((1<<k)&j)!=0&&i!=k)
                    f[i][j]=min(f[i][j],f[k][j-(1<<i)]+dist(a[i],a[k]));
        }
    printf("%.2lf",f[0][(1<<(n+1))-1]);
}

最長子序列

題面描述:給出n個元素的序列,每個元素的是大小不超過8的正整數。請找出滿足下面兩個條件的最長子序列: 1. 任意兩個數字出現的次數之差的絕對值不超過1,未出現的算0次; 2. 相同的元素是連續的(在原始序列中可以不連續)。
輸入格式: 輸入共2行 第一行包含一個整數n 接下來1行n個整數表示原始序列
輸出格式: 輸出1行,表示最長子序列的長度
輸入樣例1:
輸出樣例1:
3
1 1 1
1
輸入樣例2:
輸出樣例2:
8
8 7 6 5 4 3 2 1
8
輸入樣例3:
輸出樣例3:
24
1 8 1 2 8 2 3 8 3 4 8 4 5 8 5 6 8 6 7 8 7 8 8 8
17
數據範圍: 對於30%的數據,n<=100 對於100%的數據,n<=1000。
洛谷傳送門

我一開始想到的是暴力深搜。。然後發現最多過n<=30的情況。。
然後大概想到了DP
我想到用:
f[i][j] 表示前i個數在j 這一種情況下的最優值,但是我發現,這道題這樣子DP是有後效性的,所以這種方法不可行。
同時你開始會你會發現,每種數字如果要取,只會有三種情況,數量x,x+1,x1 ,因爲如果無法滿足這個性質那麼可以直接輸出這個序列包含的數字種數。
那麼,我們可以去先枚舉這個x,因爲數據範圍告訴我們,x最多不會超過n/8=125 個,再加上時限有2s,所以可以輕鬆搭dp,(後來發現這裏要二分,因爲如果大的x可以,那麼小的x一定可以)

那麼此時,我們的f[i][j]ij 的意義保持不變,f[i][j] 存的就是,我們每次枚舉的x情況下,可以取x+1 個的情況。(因爲x1 會在上一次枚舉過)

我們用一個vector類型的數組來記錄每一個數的每一個出現位置,同時很容易可以發現,取相同數字的相鄰位置,必定比越過某個相同數字更優,舉例如下

11213
當x=2時,我們肯定取的是112的前兩個11,而不是1121的開頭結尾兩個1,
這個不懂自己可以舉點例子推一推。

然後,

    for (int i=0; i<n; i++)
      {
      for (int j=0; j<mm; j++)
        if (f[i][j]!=-INF)
          for (int k=0;k<8;k++)
            if ((j&(1<<k))==0)
              {
                int dep=x+number[k]-1;
                if (dep>=num[k].size()) continue;
                f[num[k][dep]+1][j|(1<<k)]=max(f[i][j],f[num[k][dep]+1][j|(1<<k)]);//不取
                dep++;//次數++,則下一個取
                if (dep>=num[k].size()) continue;//如果越過最大次數就不做
                f[num[k][dep]+1][j|(1<<k)]=max(f[i][j]+1,f[num[k][dep]+1][j|(1<<k)]);
              }
       number[a[i+1]-1]++;
      }

這一段代碼就是狀態轉移了,每次x個數字後面的第x+1個數字取或者不取,如上。
然後我們每次做完一次,這個數字就出現過一次了,用一個number數組記錄它出現的次數,用以繼承下一次這個數字的開始位置。
沒了,再加點二分優化一下?嘻嘻

#include<bits/stdc++.h>
using namespace std;
const int mm=1<<8;
const int INF=0x3f3f3f3f;
int a[1010],f[1010][mm],number[1010],n,m,ans1,ans;
vector <int> num[1010];
int judge(int x)
{
    f[0][0]=0;
    for (int i=0; i<n; i++)
      {
      for (int j=0; j<mm; j++)
        if (f[i][j]!=-INF)
          for (int k=0;k<8;k++)
            if ((j&(1<<k))==0)
              {
                int dep=x+number[k]-1;
                if (dep>=num[k].size()) continue;
                f[num[k][dep]+1][j|(1<<k)]=max(f[i][j],f[num[k][dep]+1][j|(1<<k)]);
                dep++;
                if (dep>=num[k].size()) continue;
                f[num[k][dep]+1][j|(1<<k)]=max(f[i][j]+1,f[num[k][dep]+1][j|(1<<k)]);
              }
       number[a[i+1]-1]++;
      }
    int ans2=-INF;
    for (int i=0; i<=n; i++)
      ans2=max(ans2,f[i][mm-1]);
    if (ans2==-INF) return -1; else return x*8+ans2;
}
int main()
{
    //freopen("longseq.in","r",stdin);
    //freopen("longseq.out","w",stdout);
    scanf("%d",&n);
    for (int i=1; i<=n; i++)
      {
        scanf("%d",&a[i]);
        num[a[i]-1].push_back(i-1);
      }
    for (int i=1; i<=n>>3; i++)
      {
        for (int j=0; j<=n; j++)
          for (int k=1; k<mm; k++)
            f[j][k]=-INF;
        memset(number,0,sizeof(number));
        int ans1=judge(i);
        if (ans1!=-1) ans=max(ans,ans1);
      }
    if (ans==0)
      for (int i=0; i<8; i++)
        if (num[i].size()) ans++;
    printf("%d",ans);
}

高維宇宙

題目描述:
這是一個, 被戰鬥因果所支配的
將宇宙的命運鑽開風洞的男人的故事——。
在遙遠的太古時代……, 某羣螺旋族人發現了一個重大的事實。螺旋力進化的最終結果, 就是宇宙的
滅亡, 等待他們的只有螺旋神怒“Spiral-Nemesis”
對此感到恐懼的這羣螺旋族人, 爲了防止宇宙的崩壞, 消滅了大量的持有螺旋力量的族人, 並將所剩
無幾的生命囚禁於宇宙的角落裏。同時, 他們也停止了自身的進化, 將自己封閉在了不同於次元軸世界
的隔絕宇宙中。從此以Anti-Spiral 自稱的他們, 繼續與倖存下來的螺旋族人戰鬥着。衆多螺旋的戰士向
Anti-Spiral 發起挑戰並敗下陣來。
西蒙他們將地球託付給羅修之後, 乘上被命名爲“超銀河大Gurren”的流星聖殿, 向Anti-Spiral 的
母星進發。所在場所長期不明的Anti-Spiral 的母星, 由西蒙送給妮亞的戒指而回應了螺旋索敵反應。
螺旋索敵反應會傳達回一個長度爲n 的序列,a1::an。
西蒙知道Anti-Spiral 的母星所處在的宇宙的維度恰好就是a1::an 這n 個數兩兩配對所能形成的最
多的質數的個數。
所謂配對是指選出一個ai 和一個aj 進行配對,一個配對將形成一個新的數ai + aj
對於每一個ai 最多出現在一個配對之中,當然也可以不出現在任意一個配對中。
現在西蒙想快速知道Anti-Spiral 的母星所處在的宇宙的維度
Input
第一行一個數,n
第二行n 個數a1::an
Output
一行一個數,表示Anti-Spiral 的母星所處在的宇宙的維度
Example
prime.in prime.out
52
9 11 12 37
2
Explanation
2 和9 進行配對,2 + 9 = 11
11 和12 進行配對,11 + 12 = 23
Scoring
• 對於30% 的數據,n 10,2 ai 100。
• 對於另外30% 的數據,n 40,2 ai 200。
• 對於100% 的數據,n 40,2 ai 1000。

題目概述:
給你一堆數,每個數只能用一遍,有多少對數(兩個)的和爲質數,求最大對數。

這道題呢,倒也還好。
你很容易看出來,這是一個二分圖匹配(我打了半天DP發現這只是在用遞推來處理二分圖嚶嚶嚶)
最大匹配數,每個數又只能用一次
而且,奇數+奇數肯定是合數,偶數+偶數也肯定是合數
先歐拉篩
就把奇數一列,偶數一列,連邊,二分圖匹配。1的情況特判

#include <bits/stdc++.h>
using namespace std;
#define Rint register int
typedef long long ll;
const int LIM=5000;
int pms[LIM+1],pt;
bool vstd[LIM+1];
int n;
int od[52],ev[52],ot=0,vt=0;
int hd[52],nxt[1710],eds[1710],et=0;
void ad(int u,int v){
    eds[++et]=v;
    nxt[et]=hd[u];
    hd[u]=et;
}
int p[52];
bool iss[52];
bool dfs(int u){
    for (Rint i=hd[u];i;i=nxt[i])
    if (!iss[eds[i]]){
        iss[eds[i]]=true;
        if (!p[eds[i]]||dfs(p[eds[i]])){
            p[u]=eds[i],p[eds[i]]=u;
            return true;
        }
    }
    return false;
}
int main(){
    freopen("prime.in","r",stdin);
    freopen("prime.out","w",stdout);
    for (Rint i=2;i<=LIM;i++){
        if (!vstd[i])
            pms[++pt]=i;
        for (Rint j=1;j<=pt&&i*pms[j]<=LIM;j++){
            vstd[i*pms[j]]=true;
            if (i%pms[j]==0)break;
        }
    }
    scanf("%d",&n);
    int t1;
    for (Rint i=1;i<=n;i++){
        scanf("%d",&t1);
        if (t1&1)
            od[++ot]=t1;
        else
            ev[++vt]=t1;
    }
    for (Rint i=1;i<=vt;i++)
        for (Rint j=1;j<=ot;j++)
            if (!vstd[ev[i]+od[j]]){
                ad(i,j+vt);
            }
    int res=0;
    for (Rint i=1;i<=vt;i++)
        if (!p[i]){
            memset(iss,0,sizeof(iss));
            res+=dfs(i);
        }
    printf("%d",res); 
    return 0;
}

那麼,因爲這道題n才40,所以最大匹配數是20,那麼奇數偶數先分開,數量少的那部分作爲枚舉的狀態。
假設左邊有k 個點,右邊有n-k 個點
設狀態爲f[i][opt]
i 表示當前做完左邊集合中i 個點之後
opt 表示右邊集合中n-k 的點的狀態,用二進制數表示
這個二進制中第j 位的0/1 表示右邊集合中第i 個點沒有
匹配和已經匹配兩種狀態
轉移1:若f[i][opt] 這個狀態可行,則枚舉左集第i + 1 點
匹配右集中第j 個點
f[i+1][opt|2j]=f[i][opt] 即j這點選
轉移2:f[i+1][opt]=f[i][opt] j這個點不選
最終答案是max(f[k][opt]*count(opt)),0=< opt<2nk
count 表示opt 二進制表示中有幾位是1
f[i][j] 里根據我的理解應該是前i個數的匹配裏j這種情況是否可行,0/1.

#include <bits/stdc++.h> 
#include <iostream>
#include <cstdio>
#define fst first
#define sec second
#define mp make_pair

using namespace std;

typedef long long LL;
typedef long double LD;

const int N = 101000;
int l, a[N], b[N];
bool f[1001000], key[42][2001000];

struct egde {
    int x, n;
} e[N];

void add(int x, int y) {
    e[++l].x = y; e[l].n = e[x].n; e[x].n = l;
}

int getin() {
    char ch;
    while (!isdigit(ch = getchar()) && ch != '-');
    int x = ch == '-' ? 0 : ch - '0';
    int opt = ch == '-' ? -1 : 1;
    while (isdigit(ch = getchar())) x = x * 10 + ch - '0';
    return x * opt;
}

int main()
{
    freopen("prime.in", "r", stdin);
    freopen("prime.out", "w", stdout);

    int n = getin();
    for (int i = 1; i <= n; i++) {
        int x = getin();
        if (x & 1) a[++a[0]] = x;
        else b[++b[0]] = x;
    }

    int nx = min(a[0], b[0]);
    if (b[0] < a[0]) {
        for (int i = 0; i <= nx; i++)
            swap(a[i], b[i]);
        for (int i = nx + 1; i <= b[0]; i++)
            b[i] = a[i];
    }

    memset(f, 1, sizeof(f));
    for (int i = 2; i <= 1000000; i++)
        if (f[i]) {
            for (int j = i + i; j <= 1000000; j += i) f[j] = 0;
        }

    l = n;  
    for (int i = 1; i <= b[0]; i++)
        for (int j = 1; j <= a[0]; j++)
            if (f[b[i] + a[j]]) add(i, j);

    memset(key, 0, sizeof(key));
    key[0][0] = 1;
    for (int i = 0; i < b[0]; i++) {
        for (int j = 0; j < (1 << nx); j++)
            if (key[i][j]) {
                key[i + 1][j] = 1;
                for (int x = e[i + 1].n; x; x = e[x].n) {
                    int k = e[x].x - 1;
                    key[i + 1][j | (1 << k)] = 1;
                }
            }
    }
    int ans = 0;
    for (int j = 0; j < (1 << nx); j++)
        if (key[b[0]][j]) ans = max(ans, __builtin_popcount(j));
    printf("%d\n", ans);

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