• 状态压缩dp


    状态压缩dp

    1. 算法分析

    状压dp类型:

    1. 连通性状压dp(棋盘类dp)
    2. 集合类dp

        连通性dp的状态压缩表示的是每个点的位置关系,集合类dp的状态压缩表示的是每个点的是否存在

    状压dp特点:
        处理的棋盘的规模很小,一般n、m规模都在60以内

    2. 典型例题

    2.1 连通性状压dp

    acwing291蒙德里安的梦想
    题意: 求把N * M的棋盘分割成若干个1 * 2的的长方形,有多少种方案。
    例如当N=2,M=4时,共有5种方案。当N=2,M=3时,共有3种方案。
    如下图所示:
    Image.jpg
    1≤N,M≤11
    题解: 仔细考虑就可以知道全部的方案是取决于横的方块的方案,一旦横的方块确定后竖的方块也就确定了。使用f[i][j]表示前i-1列填完,第i列为j的情况,那么能够合法填充的j和k满足,j&k==0且j|k之间连续的1为偶数。那么考虑dp的转移方程为:f[i][j]+= f[i-1][k] (k是和j能够匹配的合法方案)
    代码:

    #include<bits/stdc++.h>
    using namespace std;
    
    typedef long long LL;
    
    int n, m;
    int const N = 1e4 + 10;
    LL f[20][N];
    int st[N];
    vector<int> state[N];
    
    int main()
    {
        while (cin >> n >> m && n && m)
        {
            // 初始化
            memset(f, 0, sizeof f);
            f[0][0] = 1;
            for (int i = 0; i < 1 << n; ++i) state[i].clear();
    
            // 预处理st
            for (int i = 0; i < 1 << n; i ++ )
            {
                int cnt = 0;
                st[i] = true;
                for (int j = 0; j < n; j ++ )
                    if (i >> j & 1)
                    {
                        if (cnt & 1) st[i] = false;
                        cnt = 0;
                    }
                    else cnt ++ ;
                if (cnt & 1) st[i] = false;
            }
    
            // 预处理state
            for (int i = 0; i < 1 << n; ++i)
            {
                for (int j = 0; j < 1 << n; ++j)
                {
                    if ((i & j) == 0 && st[i | j]) state[i].push_back(j);
                }
            }
    
            // dp转移
            for (int i = 1; i <= m; ++i)
            {
                for (int j = 0; j < 1 << n; ++j)
                {
                    for (int k = 0; k < state[j].size(); ++k)  // 获得所有的合法方案
                    {
                        f[i][j] += f[i - 1][state[j][k]];
                    }
                }
            }
            printf("%lld
    ", f[m][0]);
        }
        
        return 0;
    }
    

    acwing1064骑士
    题意: 在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。1≤n≤10,0≤k≤n^2^
    题解: 状压dp模板题
    代码:

    #include<bits/stdc++.h>
    
    using namespace std;
    
    typedef long long LL;
    
    const int N = 12, M = 1 << 10, K = 110;
    
    int n, m;
    vector<int> state;
    int cnt[M];
    vector<int> head[M];
    LL f[N][K][M];
    
    // 计算每种情况是否合法
    bool check(int state)
    {
        for (int i = 0; i < n; i ++ )
            if ((state >> i & 1) && (state >> i + 1 & 1))
                return false;
        return true;
    }
    
    // 计算每种情况内的1
    int count(int state)
    {
        int res = 0;
        for (int i = 0; i < n; i ++ ) res += state >> i & 1;
        return res;
    }
    
    int main()
    {
        cin >> n >> m;
    
        // 记录所有合法的情况,同时计算出每种合法情况的1数目
        for (int i = 0; i < 1 << n; i ++ )
            if (check(i))
            {
                state.push_back(i);
                cnt[i] = count(i);
            }
    
        // 计算哪两种合法情况间能够互相匹配
        for (int i = 0; i < state.size(); i ++ )
            for (int j = 0; j < state.size(); j ++ )
            {
                int a = state[i], b = state[j];
                if ((a & b) == 0 && check(a | b))
                    head[i].push_back(j);
            }
    
        // 状态转移:f[i][j][a]:到第i行,使用了j个,第i行填充a
        f[0][0][0] = 1;  // 本题入口与蒙德里安的梦想不同是因为本题的第一列可以填东西,而蒙德里安的梦想第一列不可以填东西
        for (int i = 1; i <= n + 1; i ++ )
            for (int j = 0; j <= m; j ++ )
                for (int a = 0; a < state.size(); a ++ )
                    for (int b : head[a])  // 第i-1行填充b
                    {
                        int c = cnt[state[a]];
                        if (j >= c)
                            f[i][j][a] += f[i - 1][j - c][b];
                    }
    
        cout << f[n + 1][m][0] << endl;  // 输出第n+1行,使用了m个,第n+1行填充0的情况
    
        return 0;
    }
    

    acwing327玉米田
    题意: 农夫约翰的土地由M*N个小方格组成,现在他要在土地里种植玉米。非常遗憾,部分土地是不育的,无法种植。而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。现在给定土地的大小,请你求出共有多少种种植方法。土地上什么都不种也算一种方法。1≤M,N≤12
    题解: 状压dp模板题
    代码:

    #include<bits/stdc++.h>
    
    using namespace std;
    
    typedef long long LL;
    int n, m;
    int const N = 14, M = 1 << N, MOD = 1e8;
    LL f[N][M];
    int g[N];
    vector<int> st;
    vector<int> state[M];
    
    bool check(int x)
    {
        for (int i = 0; i < m - 1; ++i)
            if ((x >> i & 1) && (x >> (i + 1) & 1)) return false;
        return true;
    }
    
    int main()
    {
        // 初始化玉米田地
        cin >> n >> m;
        for (int i = 1 ; i <= n; ++i)
        {
            for (int j = 0; j < m; ++j)
            {
                int t;
                cin >> t;
                g[i] += (!t << j);
            }
        }
    
        // 找到所有合法方案
        for (int i = 0; i < 1 << m; ++i)
        {
            if (check(i)) st.push_back(i);
        }
    
        // 找到可以互相匹配的合法方案
        for (int i = 0; i < st.size(); ++i)
            for (int j = 0; j < st.size(); ++j)
            {
                int a = st[i], b = st[j];
                if ((a & b) == 0) state[i].push_back(j);
            }
    
        // 初始化
        memset(f, 0, sizeof f);
        f[0][0] = 1;
    
        // 状态转移f[i][j]:第i行使用第j种合法方案
        for (int i = 1; i <= n + 1; i ++ )  // 第i行
            for (int j = 0; j < st.size(); j ++ )  // 第j种合法方案
                if (!(st[j] & g[i])) // 如果第j种合法方案和当前玉米地不匹配
                    for (int k : state[j])  // 第i-1行选择合法方案k
                        f[i][j] = (f[i][j] + f[i - 1][k]) % MOD;
    
        cout << f[n + 1][0] << endl;  // 第n+1行选择合法方案0的情况(即000000)
    
        return 0;
    }
    

    acwing292 炮兵阵地
    题意: 司令部的将军们打算在N * M的网格地图上部署他们的炮兵部队。一个N * M的地图由N行M列组成,地图的每一格可能是山地(用”H” 表示),也可能是平原(用”P”表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
    Image 2.jpg
    如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。输出最多能摆放的炮兵部队的数量。N≤100,M≤10
    题解:

    代码:

    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #include <vector>
    
    using namespace std;
    
    const int N = 10, M = 1 << 10;
    
    int n, m;
    int g[1010];
    int f[2][M][M];
    vector<int> state;
    int cnt[M];
    
    bool check(int state)
    {
        for (int i = 0; i < m; i ++ )
            if ((state >> i & 1) && ((state >> i + 1 & 1) || (state >> i + 2 & 1)))
                return false;
        return true;
    }
    
    int count(int state)
    {
        int res = 0;
        for (int i = 0; i < m; i ++ )
            if (state >> i & 1)
                res ++ ;
        return res;
    }
    
    int main()
    {
        cin >> n >> m;
        for (int i = 1; i <= n; i ++ )
            for (int j = 0; j < m; j ++ )
            {
                char c;
                cin >> c;
                g[i] += (c == 'H') << j;
            }
    
        for (int i = 0; i < 1 << m; i ++ )
            if (check(i))
            {
                state.push_back(i);
                cnt[i] = count(i);
            }
    
        for (int i = 1; i <= n; i ++ )
            for (int j = 0; j < state.size(); j ++ )
                for (int k = 0; k < state.size(); k ++ )
                    for (int u = 0; u < state.size(); u ++ )
                    {
                        int a = state[j], b = state[k], c = state[u];
                        if (a & b | a & c | b & c) continue;
                        if (g[i] & b | g[i - 1] & a) continue;
                        f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[b]);
                    }
    
        int res = 0;
        for (int i = 0; i < state.size(); i ++ )
            for (int j = 0; j < state.size(); j ++ )
                res = max(res, f[n & 1][i][j]);
    
        cout << res << endl;
    
        return 0;
    }
    

    2.2 集合类dp

    acwing 91. 最短Hamilton路径
    题意: 给定一张 n 个点的带权无向图,点从 0 ~ n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。输出一个整数,表示最短Hamilton路径的长度。1≤n≤20,0≤a[i,j]≤10^7^
    题解: 本题如果纯暴力那么共有n!种情况
    因此可以考虑使用二进制来优化时间复杂度。第一维枚举每个点是否被使用过,第二维和第三维分别枚举当前是在哪个点上,通过不断的三角不等式更新,最后就可以取得最小值。这个题本质就是对floyd的一种优化
    代码:

    #include<bits/stdc++.h>
    
    using namespace std;
    
    int const N = 1 << 20;
    int f[N][21];
    int n;
    int g[21][21];
    int main()
    {
        // 读入边
        cin >> n;
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < n; ++j)
                cin >> g[i][j];
        
        // 初始化
        memset(f, 0x3f, sizeof f);
        f[1][0] = 0;
    
        // 更新f数组
        for (int i = 0; i < 1 << n; ++i)  // 枚举每一种情况
        {
            for (int j = 0; j < n; ++j)  // 枚举第1个点
            {
                if (i >> j & 1)
                {
                    for (int k = 0; k < n; ++k)  // 枚举第2个点
                    {
                        if ((i - (1 << j)) >> k & 1)
                        {
                            f[i][j] = min(f[i][j], f[i - (1 << j)][k] + g[k][j]);  // 三角不等式更新
                        }
                    }
                }
            }
        }
        cout << f[(1 << n) - 1][n - 1] << endl;
        return 0;
    }
    
  • 相关阅读:
    2015年5月1日 转载--各种变量在内存中的分布
    2015年4月30日 计算两个日期天数,写的代码
    2015年4月29日 dayofweek
    2015年4月28日
    2015年4月28日----高大上的数组,进制准换,最多是35进制
    2015年4月27日---C语言:输出特殊图案,请在c环境中运行,看一看,Very Beautiful!
    2015年4月27日
    《C++ Primer》学习笔记:迭代器介绍
    《C++ Primer》学习笔记:3.3.3其他vector操作
    《C++ Primer》学习笔记:向vector对象添加元素蕴含的编程假定
  • 原文地址:https://www.cnblogs.com/spciay/p/13513144.html
Copyright © 2020-2023  润新知