状吔DP get !

· · 个人记录

normal

传送门

求方案数

个人感觉,

难点在预处理上呢。。。

主要还是预处理,要记录每种状态用了多少棋子 预处理其实是给每种状态从 1 ~ cnt 编号 $dp$ 的中间标号也是每个状态的编号 这样就可以减小常数了 虽然不是很难,但作为第一道题还是费了一番工夫的 ```cpp long long dp[10][1000][100]; int num[1000],g[1000]; //状态 & 个数 int count(int x) { int ans = 0; while(x) { ans += (x&1); x >>= 1; } return g[cnt] = ans; } int main(void) { cin >> n >> k; int MAXN = (1<<n)-1; for(int i=0;i<=MAXN;i++) { if(i & (i<<1)) continue; num[++cnt] = i; dp[1][cnt][count(i)] = 1; } for(int i=2;i<=n;i++) for(int j=1;j<=cnt;j++) for(int J=1;J<=cnt;J++) { int x = num[j], y = num[J]; if((x&y) || ((x<<1)&y) || (x&(y<<1))) continue; for(int s=0;s<=k;s++) dp[i][j][g[j]+s] += dp[i-1][J][s]; } for(int i=1;i<=cnt;i++) tot += dp[n][i][k]; cout << tot; return 0; } ``` # $hard$: [传送门](https://www.luogu.org/problemnew/show/P2704) **求最值** 因为这道题的转移与前两行有关, 所以 $dp$ 存这一行状态与前一行状态 ```cpp #include <stdio.h> #include <iostream> using namespace std; int m,n,cnt,tot; char c; int map[105],num[1000],g[1000]; int dp[1000][1000][100];//前一行状态 此行状态 行数 int get(int x) { int ans = 0; while(x) { ans += (x&1); x >>= 1; } return ans; } int main(void) { cin >> n >> m; int MAXN = (1<<m)-1; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { cin >> c; map[i] <<= 1; if(c == 'H') map[i] |= 1; } for(int i=0;i<=MAXN;i++) { if(i&(i<<1) || i&(i<<2)) continue; num[++cnt] = i; g[cnt] = get(i); if(i & map[1]) continue; dp[0][cnt][1] = g[cnt]; } for(int i=1;i<=cnt;i++) for(int j=1;j<=cnt;j++) { if(num[i]&num[j] || num[j]&map[2]) continue; dp[i][j][2] = max(dp[i][j][2],dp[0][i][1]+g[j]); } for(int i=3;i<=n;i++) for(int h=1;h<=cnt;h++) { if(num[h]&map[i]) continue; for(int y=1;y<=cnt;y++) { if(num[y]&num[h]) continue; for(int e=1;e<=cnt;e++) { if(num[e]&num[y] || num[e]&num[h]) continue; dp[y][h][i] = max(dp[y][h][i],dp[e][y][i-1]+g[h]); } } } for(int i=1;i<=cnt;i++) for(int j=1;j<=cnt;j++) tot = max(tot,dp[i][j][n]); cout << tot; return 0; } ``` # $Ex$: [传送门](https://www.luogu.org/problemnew/show/P2258#sub) 思路想两秒,代码调两天系列。。。 看到题面后,第一反应就是要状压, 怎么状压呢?当然是枚举列的状态,再枚举行的状态,同时 $DP$ 求最值! 然后就开始了漫长的调代码阶段。。。 最后,感谢[system_has_collapsed](https://www.luogu.org/space/show?uid=56302)奆佬的题解! ```cpp int n,m,c,r,ans=0x3f3f3f3f; int map [20][20],len [20]; int w[20],v[20][20],f[20][20]; void dp() { memset(w,0 ,sizeof(w)); memset(v,0 ,sizeof(v)); memset(f,0x3f,sizeof(f)); f[0][0] = 0; for(int i= 1;i<=m;i++) //统计每列的绝对值和 for(int j= 1;j< c;j++) w[i] += abs(map[len[j]][i] - map[len[j+1]][i]); for(int i= 1;i< m;i++) //统计每行每两个数的差 for(int j=i+1;j<=m;j++) for(int k= 1;k<=c;k++) v[i][j] += abs(map[len[k]][i] - map[len[k]][j]); for(int i= 1;i<=r;i++) //DP 当前状态的最优值 for(int j= i;j<=m;j++) for(int k= 0;k< j;k++) f[i][j] = min(f[i][j],w[j]+v[k][j]+f[i-1][k]); for(int i=r;i<=m;i++) //找最小值 ans = min(ans,f[r][i]); } void dfs(int k, int x) { if(k >= c){ dp(); return; } for(int i=x+1;i<=n;i++){ len[k+1] = i; dfs(k+1,i); } } int main(void) { cin >> n >> m >> c >> r; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin >> map[i][j]; dfs(0,0); cout << ans; return 0; } ``` # $TSP$: [传送门](https://www.luogu.org/problemnew/show/P1171) 著名的旅行商问题,没有在多项式时间内求解的算法,不过在小数据范围内,还是可以用状吔 $DP$ ~~和模拟退火~~过的 $f[i][j]$ 表示选了状态为 $i$ 的村庄,最后在 $j$ 点的最短路程 ```cpp int map[25][25]; int f[MAXN][25]; int n,ans=0x3f3f3f3f; inline void dp() { for(int i=0;i<=(1<<n)-1;i++) for(int j=1;j<=n;j++) { if((1<<(j-1)) & i) continue; for(int k=1;k<=n;k++) if((1<<(k-1)) & i) f[i|(1<<(j-1))][j] = min(f[i][k]+map[k][j],f[i|(1<<(j-1))][j]); } } int main(void) { memset(f,0x3f,sizeof f); f[1][1] = 0; cin >> n; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&map[i][j]); dp(); for(int i=2;i<=n;i++) ans = min(ans,f[(1<<n)-1][i]+map[i][1]); cout << ans; return 0; } ``` # 类似的: [传送门](https://www.luogu.org/problemnew/show/UVA10944) $emmmmmm...

不得不吐槽一下这个老鼠蛋蛋。。。。(美国人就是社会)

struct node{
    int x,y;
}point[30];

inline void dp()
{
    for(int i=0;i<(1<<cnt)-1;i++)
    for(int j=1;j<=cnt;j++)
    {
        if(i & 1<<(j-1)) continue;
        for(int k=1;k<=cnt;k++)
        {
            if((1<<(k-1)) & i)
                f[i|(1<<(j-1))][j] = min(f[i|(1<<(j-1))][j],f[i][k]+map[j][k]);
        }
    }
}

int main(void)
{
    while(scanf("%d%d",&n,&m) != EOF)
    {
        cnt = 1;
        memset(f,0x3f,sizeof f);
        memset(map,0,sizeof map);
        f[1][1] = 0;
        for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            char c;
            cin >> c;
            if(c == 'L')    point[1] = (node){i,j};
            if(c == '#')    point[++cnt] = (node){i,j};
        }
        for(int i=1;i<=cnt;i++)
        for(int j=i+1;j<=cnt;j++)
            map[i][j] = map[j][i] = max(abs(point[i].x-point[j].x),abs(point[i].y-point[j].y));
        dp();
        ans = 0x3f3f3f3f;
        for(int i=1;i<=cnt;i++)
            ans = min(ans,f[(1<<cnt)-1][i]+map[i][1]);
        cout << ans << endl;
    }
    return 0;
}

搜索:

传送门

当转移不好想或顺序不好确定的时候可以利用状压搜索来代替。

int n,m,ans=0x3f3f3f3f;
int f[1<<MAXN],dis[MAXN];
int map[MAXN][MAXN];

void dfs(int x)
{
    for(int i=1;i<=n;i++)
    {
        if((x&1<<(i-1)) == 0) continue;
        for(int j=1;j<=n;j++)
        {
            if(x&1<<(j-1) || map[i][j]==0x3f3f3f3f) continue;
            if(f[x|1<<(j-1)] > f[x]+dis[i]*map[i][j])
            {
                int temp = dis[j];
                f[x|1<<(j-1)] = f[x]+dis[i]*map[i][j];
                dis[j] = dis[i]+1;
                dfs(x|1<<(j-1));
                dis[j] = temp;
            }
        }
    }
}

signed main(void)
{
    memset(map,0x3f,sizeof map);
    cin >> n >> m;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        cin >> u >> v >> w;
        map[u][v] = map[v][u] = min(map[u][v],w);
    }
    for(int i=1;i<=n;i++)
    {
        memset(f  ,0x3f,sizeof f  );
        memset(dis,0x3f,sizeof dis);
        dis[i] = 1;
        f[1<<(i-1)] = 0;
        dfs(1<<(i-1));
        ans = min(ans,f[(1<<n)-1]);
    }
    cout << ans;
    return 0;
}

逆向:

传送门

多数状压都是用 或 来累加状态,而这道题则是用 异或 来扣去一个状态进行转移。

f[i] 表示在 i 状态下最多能买到第几个物品

怎么转移呢?

可以发现,如果用 1<<(i-1) 来表示第 i 个硬币的话,每种状态都可以扣去某一位的 1 表示没用这枚硬币前能买到第几个物品。

那么,记一个前缀和 len 表示物品总价,设 len 表示上一个状态能买到第几个物品,在使用第 j 个硬币时,当前状态就是 sum[len] + con[j] 所能买到的物品数。

int n,m,tot,ans=-1,Mx,cnt;
int con[20],state[20];
int sum[MAXN];
int f[1<<20];

int main(void)
{
    cin >> n >> m;
    Mx = (1<<n)-1;
    for(int i=1;i<=n;i++)
    {
        cin >> con[i];
        tot += con[i];
        state[i] = 1<<(i-1);
    }
    for(int i=1;i<=m;i++)
    {
        cin >> sum[i];
        sum[i] += sum[i-1];
    }
    for(int i=1;i<=Mx;i++)
    for(int j=1;j<=n;j++)
    {
        if(!(state[j]&i)) continue;
        int len = f[i^state[j]];
        len = upper_bound(sum+1,sum+1+m,sum[len]+con[j])-sum-1;
        f[i] = max(f[i],len);
    }
    for(int i=1;i<=Mx;i++)
    {
        if(f[i] != m) continue;
        cnt = 0;
        for(int j=1;j<=n;j++)
            if(i & state[j])
                cnt += con[j];
        ans = max(ans,tot-cnt);
    }
    cout << ans;
    return 0;
}

巩固:

传送门

与上一题类似。不过上一题用的填表法,这道题我用了刷表法

定义 f[i] 表示在 i 集合中最远能到达的位置。

枚举要填入那种电影,二分的找这种电影小于 f[i] 且最晚的开始时间,因为时长固定,所以越晚开始必定约优。

#define re register

int n,len,ans=100;
int a[22][MAXN],lenth[MAXN];
int f[1<<22];

inline void solve()
{
    for(re int i=0;i<=(1<<n)-1;i++)
    {
        if(f[i] == -1) continue;
        for(re int j=1;j<=n;j++)
        {
            if(i & (1<<(j-1))) continue;
            int now = upper_bound(a[j]+1,a[j]+1+a[j][0],f[i])-a[j]-1;
            if(now <= 0) f[i|(1<<(j-1))] = f[i];
            else f[i|(1<<(j-1))] = max(f[i|(1<<(j-1))],a[j][now]+lenth[j]);
        }
        if(f[i] >= len) ans = min(ans,__builtin_popcount(i));
    }
}

signed main(void)
{
    memset(f,-1,sizeof f);
    f[0] = 0;
    scanf("%d%d",&n,&len);
    for(re int i=1;i<=n;i++)
    {
        scanf("%d%d",&lenth[i],&a[i][0]);
        for(re int j=1;j<=a[i][0];j++)
            scanf("%d",&a[i][j]);
    }
    solve();
    if(ans == 100) puts("-1");
    else cout << ans;
    return 0;
}