状态压缩DP

一、小国王

题目链接
在这里插入图片描述
图解:
在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
const int N = 12, M = 1 << N, K = 110;
vector<int> state; //记录每一行合法状态
int cnt[M]; //记录每行对应的合法状态的国王数
vector<int> head[M];//记录i - 1映射的i行所对应的合法状态
typedef long long LL;
LL f[N][K][M];//记录每一行的状态所对应的国王数
int n,m;

bool check(int u)
{
    for(int i = 0;i < n;i ++)
    if((u >> i & 1) && (u >> i + 1 & 1))//出现连续的1的状态就不满足条件
    return false;
    
    return true;
}

int count(int u)
{
    int res = 0;
    for(int i = 0;i < n;i ++)
    if(u >> i & 1) res ++;
    
    return res;
}

int main()
{
    
    cin >> n >> m;//输入行,国王数
    
    //先把每一行的合法状态存起来,然后你才能记录i - 1映射的i行所对应的合法状态
    for(int i = 0;i < 1 << n;i ++)
    if(check(i))//检查是否合法
    {
        state.push_back(i);
        cnt[i] = count(i);//记录合法状态对应的国王数目
    }
    
    //然后记录i-1映射的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))//a & b是特判i == j的情况,这个情况一定不满足i-1行和i行所映射关系的条件
            head[i].push_back(j);
        }
    }
    f[0][0][0] = 1;//第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 ++)/*第i行的每一种合法状态的枚举
            然后找出第i行所映射的i-1行的合法状态,这样就可以通过i-1行的状态转移到第i行的状态*/
                for(int b : head[a])//取出i-1行所对应的合法状态
                {
                    //还要判断i行的国王数目是否大于当前j状态枚举的国王数目
                    //只有大于当前j状态枚举的国王数目,你才能够满足从上个状态转移过来的国王数,不然的话你上一行的国王数目是负数
                    //很显然这是不可能的
                    int c = cnt[state[a]];
                    if(j >= c)//大于的话就可以转移过来
                    {
                        f[i][j][state[a]] += f[i - 1][j - c][state[b]];
                    }
                }
    
    cout << f[n + 1][m][0];//m个国王已经在前n行摆好了
    
    return 0;
}

二、玉米田

题目链接
在这里插入图片描述

这个题和上面一个题很相似,上面一个题弄懂个这个题基本上每什么难度

在这里插入图片描述

#include<iostream>
#include<vector>
using namespace std;
const int N = 15, M = 1 << 12;
vector<int> state;//每行所满足的状态
vector<int> head[M];//存储本行的状态所映射的上一行的状态不冲突的情况
typedef long long LL;
LL f[N][M];//存储每一行的所满足条件的方案数目
int w[N];//存储每行的状态
int n,m;//n代表列数,m代表行数
const int mod = 1e8;

//检查函数是否有两个相邻的1
bool check(int u)
{
    for(int i = 0;i < n;i ++)
    if((u >> i & 1) && (u >> i + 1 & 1))
    return false;
    
    return true;
}

int main()
{
    
    cin >> m >> n;
    for(int i = 1;i <= m;i ++)
        for(int j = 0;j < n;j ++)
        {
            int k;
            cin >> k;
            w[i] += !k * (1 << j);//这里我们把每列的0位置当作可以种植,1当作不能种植
        }
        
    //然后我们把state状态里面1当作在w的0上面种植了,0当作在w上面不能种植也就是 
    //!(state & w)代表满足在不能种植的地方不种植,在种植的地方种植
    
    //列举每个满足条件的种植状态
    for(int i = 0;i < 1 << n;i ++)
    if(check(i))
    state.push_back(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))//i & j为0也就证明第i行和第i-1行同列的田地不能同时种植玉米
            head[i].push_back(j);
        }
    
    f[0][0] = 1;
    for(int i = 1;i <= m + 1;i ++)//每一行
        for(int j = 0;j < state.size();j ++)//每一行满足条件的状态
            if(!(state[j] & w[i]))//!(state[j] & w[i])代表满足在不能种植的地方不种植,在种植的地方种植
            {
                for(int a : head[j])
                f[i][j] = (f[i][j] + f[i - 1][a]) % mod;
            }
    
    cout << f[m + 1][0] % mod;
    return 0;
}

三、炮兵阵地

题目链接
在这里插入图片描述

这个题压缩了两层信息的状态,也就是i-1和第i层。

#include <cstring> // 引入cstring库,用于内存操作(如memset等),但本代码未直接使用memset初始化动态规划数组,而是使用了循环  
#include <iostream> // 引入iostream库,用于标准输入输出  
#include <algorithm> // 引入algorithm库,但本代码未直接使用其提供的算法  
#include <vector> // 引入vector库,用于动态数组  
  
using namespace std;  
  
// 定义常量  
const int N = 10; // 网格的行数上限(实际上未直接使用,但定义了有助于理解)  
const int M = 1 << 10; // 列数的二进制表示上限,假设列数最多为10(即1024种状态)  
  
int n, m; // n表示网格的行数,m表示列数  
int g[1010]; // 存储每行的状态,使用二进制表示,'H'为1,'.'为0(数组大小设为1010是为了容纳最多100行的输入,加上可能的边界情况)  
int f[2][M][M]; // 动态规划数组,f[i][j][k]表示第i行(使用滚动数组优化为两行),前两行状态分别为j和k时的最大炮兵数  
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) || (i + 2 < m && 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++; // 则炮兵数量加1  
    return res; // 返回炮兵数量  
}  
  
int main()  
{  
    cin >> n >> m; // 输入网格的行数和列数  
    // 读取每行的状态,'H'表示该位置有炮兵,'.'表示该位置没有炮兵  
    for (int i = 1; i <= n; i++) // 遍历每一行  
        for (int j = 0; j < m; j++) // 遍历每一列  
        {  
            char c;  
            cin >> c; // 读取当前位置的字符  
            g[i] += (c == 'H') << j; // 如果当前位置是'H',则将其对应的二进制位设为1  
        }  
  
    // 预处理所有合法的状态及其炮兵数量  
    for (int i = 0; i < (1 << m); i++) // 遍历所有可能的状态  
        if (check(i)) // 如果状态合法  
        {  
            state.push_back(i); // 将合法状态添加到state向量中  
            cnt[i] = count(i); // 计算并存储该合法状态中的炮兵数量  
        }  
  
    // 初始化动态规划数组(实际上未显式初始化为0,但由于使用了滚动数组,且每次更新都基于上一行的状态,因此初始值不会影响最终结果)  
    // 动态规划求解最大炮兵数  
    for (int i = 1; i <= n; i++) // 遍历每一行  
        for (int j = 0; j < state.size(); j++) // 遍历上一行的所有合法状态  
            for (int k = 0; k < state.size(); k++) // 遍历当前行的所有合法状态  
                // 注意:这里的u是遍历上上行的状态,但由于我们使用了滚动数组,实际上只需要考虑上一行和当前行的状态  
                // 因此,这里的u可以看作是在更新f[i][j][k]时,用来找到上一行(即f[i-1])与当前行状态j匹配的最佳上上行状态  
                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) || (i > 1 && g[i - 1] & a)) continue; // 如果当前行的状态与障碍物冲突,或者上一行的状态与上一行的障碍物冲突(注意i>1的条件,避免第一行没有上一行的情况)  
                    // 更新动态规划数组(使用滚动数组优化空间)  
                    // f[i & 1][j][k]表示第i行(使用滚动数组表示为i%2或i&1),状态为j和k时的最大炮兵数  
                    // f[i - 1 & 1][u][j]表示上一行(即i-1行,使用滚动数组表示为(i-1)%2或i-1&1),状态为u和j时的最大炮兵数(注意这里u和j的位置与更新f[i][j][k]时相反,因为我们是基于上一行的状态来更新当前行的)  
                    // cnt[b]表示当前行状态b中的炮兵数量  
                    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]); // 更新最大炮兵数(n&1用于获取最后一行在滚动数组中的索引)  
  
    cout << res << endl; // 输出最大炮兵数  
  
    return 0;  
}

四、愤怒的小鸟

题目链接
在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
const int N = 18, M = 1 << 18;
#define x first
#define y second
typedef pair<double,double> PDD;
PDD p[N];//输入每个点的坐标
int f[M];//每个状态对应的所需抛物线最少数量
int path[N][N];//每两个点构成的抛物线所覆盖的点集
int T,n,m;
const double esp = 1e-8;

int cmp(double x,double y)
{
    if (fabs(x - y) < esp) return 0;
    if (x < y) return -1;
    return 1;
}
int main()
{
    cin >> T;
    while(T --)
    {
        cin >> n >> m;
        for(int i = 0;i < n;i ++) cin >> p[i].x >> p[i].y;//输入坐标
        
        memset(path, 0, sizeof path);
        //枚举每个两个点构成的抛物线所涵盖的点
        for(int i = 0;i < n;i ++)
        {
            path[i][i] = 1 << i;//路径的点是同一个点,那么设置这条路径的状态为1 << i
            for(int j = 0;j < n;j ++)
            {
                if(i == j) continue;
                double x1 = p[i].x, y1 = p[i].y, x2 = p[j].x, y2 = p[j].y;
                
                double a = (y1/x1 - y2/x2)/(x1 - x2);
                double b = y1/x1 - a*x1;
                if(cmp(a, 0) >= 0) continue;
                
                for(int k = 0;k < n;k ++)
                {
                    double lx = p[k].x, ry = p[k].y;
                    if(!cmp(a*lx*lx+b*lx, ry))
                    path[i][j] += 1 << k;
                }
            }
        }
        memset(f, 0x3f, sizeof f);//每个状态我们要求最小值,所以我们把它设置为最大值,好求最小值
        f[0] = 0;//状态为0时刻经过0个点
        
        
        //每个抛物线经过的点集处理完之后
        //枚举每个状态的子集,看从哪个状态转移过来所需的抛物线数量最少
        for(int i = 0;i + 1 < 1 << n;i ++)
        {
            //列举每个状态
            int t;
            for(int j = 0;j < n;j ++)
            {
                if(!(i >> j & 1))
                {
                    t = j;
                    break;
                }
            }
            
            //找到该点不在该抛物线上面
            for(int k = 0;k < n;k ++)
            f[i | path[t][k]] = min(f[i | path[t][k]], f[i] + 1);
        }
        
        cout << f[(1 << n) - 1] << endl;
    }
    return 0;
}

代码图解

在这里插入图片描述

下面是优化之后的版本(用了记忆化搜索)

#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
typedef unsigned long long ULL;
typedef long long LL;
typedef pair<double,double> PDD;
const int N = 18 , M=101, MOD = 1e8 ,INF=0x3f3f3f3f;
const double eps = 1e-8;

PDD pos[N];
int cover[N][N];    //cover[i][j]表示i和j号猪组成的抛物线可以覆盖的点集
int n,m,dp[1<<N];

int comp(double a,double b)     //比较两个浮点数
{
    if(fabs(a-b)<eps) return 0;
    else if(a>b) return 1;
    return -1;
}

int dfs(int state)      //dp[i]:从状态i覆盖完所有点最少需要多少
{
    if(state+1==(1<<n)) return dp[state]=0;
    if(dp[state]) return dp[state];

    int ret=n*n;

    int t=0;

    for(int i=0;i<n;i++)
    {
        if(!(state&(1<<i)))         //如果还有未覆盖的点
        {
            t=i;
            break;
        }
    }

    for(int i=0;i<n;i++)
    {
        if(!cover[t][i]) continue;      //细节,如果这个点和t无法构成抛物线的话,递归这个状态会导致死循环
        if(state&(1<<i)) continue;
        ret = min(ret,dfs(state|cover[t][i])+1);
    }

    return dp[state] = ret;
}

void process()
{
    memset(cover,0,sizeof(cover));
    for(int i=0;i<n;i++)
    {
        cover[i][i]=(1<<i);         //自己和自己可以覆盖
        for(int j=i;j<n;j++)    //枚举两个点构成的抛物线
        {
            double x1 = pos[i].first,x2 = pos[j].first;
            double y1 = pos[i].second,y2 = pos[j].second;

            if(!comp(x1,x2)) continue;      //这里特判很容易漏,如果不特判的话,下面计算a的时候分母x1-x2就是0了

            double a = (y1/x1-y2/x2) / (x1-x2);
            double b = y1/x1-a*x1;

            if(comp(a,0)>=0) continue;      //题目要求抛物线开口向下

            for(int k=0;k<n;k++)        //然后枚举这条抛物线可以覆盖哪些点
            {
                double x=pos[k].first,y=pos[k].second;
                if(!comp(a*x*x+b*x,y))  cover[i][j]|=(1<<k);
            }
        }
    }
}

void solve()
{

    cin>>n>>m;

    for(int i=0;i<n;i++) cin>>pos[i].first>>pos[i].second;

    process();

    memset(dp,0,sizeof(dp));    
    dfs(0);
    cout<<dp[0]<<endl;
}

int main(){

    cin.tie(0), cout.tie(0);
    ios::sync_with_stdio(0);

    int T;
    cin>>T;

    while(T--)  solve();

    return 0;
}

五、宝藏

题目链接
在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 14,M = 1 << 12;
int n,m;
int g[N][N];//两个相邻的宝藏屋不算层数的代价
int f[M][N];//每个状态所在层次所需的最小值
int rg[M];//每个i位置所能遍及的位置

const int INF = 0x3f3f3f3f;

int main()
{
    cin >> n >> m;
    
    memset(g, 0x3f, sizeof g);
    
    for(int i = 0;i < n;i ++) g[i][i] = 0;//自己到自己的距离为0
    
    for(int i = 0;i < m;i ++)
    {
        int a,b,c;
        cin >> a >> b >> c;
        a --, b--;//我们从终点计算层数
        g[a][b] = g[b][a] = min(c, g[a][b]);//防止有重边
    }
    
    
    
    
    //枚举每个状态能够波及到的状态,为什么要枚举这个呢?
    //因为你下面要求子状态到达主状态所需的最小花费,也就是子状态到达主状态还需要挖掘宝藏
    //例如:主状态11001,子状态10001,你还需要挖掘第2个点才能到达11001,及你要求第二个点距离子状态所有点,求距离最小的那个点就行
    for(int i = 0;i < 1 << n;i ++)
    {
        for(int j = 0;j < n;j ++)
        {
            if(i >> j & 1)
            for(int k = 0;k < n;k ++)
            //枚举i状态的j这个点所相邻的点
            if(g[j][k] != INF)
            rg[i] |= 1 << k;
        }
    }
    
    memset(f, 0x3f, sizeof f);
    for(int i = 0;i < n;i ++) f[1 << i][0] = 0;//从地面打通到某个位置(免费)
    
    for(int i = 0;i < 1 << n;i ++)//枚举每个状态,然后枚举他们的子集,到达i这个状态所需的最小花费
    {
        for(int j = (i - 1) & i;j;j = (j - 1) & i) //求所有子集
        {
            //首先看你的rg[j]这个状态能否波及到i这个状态,能的话,你就能从j这个状态转移到i这个状态
            if( (rg[j] & i) == i)
            {
                int remain = i ^ j;
                int cost = 0;
                
                for(int k = 0;k < n;k ++)
                if(remain >> k & 1)
                {
                    int x = INF;
                    for(int t = 0;t < n;t ++)
                    if(j >> t & 1)
                    x = min(x, g[k][t]);
                    
                    cost += x;
                }
                
                //这个状态可能出现在任何一层
                for(int t = 1;t < n;t ++)
                f[i][t] = min(f[i][t], f[j][t - 1] + cost * t);//上一层的状态转移到下一层的状态
            }
            
        }
    }
    int ans = INF;
    for(int i = 0;i < n;i ++) ans = min(ans, f[(1 << n) - 1][i]);
    
    cout << ans;
    
    return 0;
}

在这里插入图片描述

y总的代码的注释

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

早睡早起^_^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值