状压DP

集合状压DP

Doing Homework HDU - 1074

题意:

1:每次给n个作业及其花费时间和截至时间
2:每次做一个作业直到做完才可以换下一个作业
3:对于每一个作业,每超过一天就会减去1分
4:问减去的最小的分数
5:题中的输入顺序是按字典序的

思路:

状态压缩动态规划
在二进制情况下,第 i 位为 1 表示已做完 为 0 表示未做
这样可以用最多 \(2^{15}\) 的数来表示所有情况

过程:
\(dp[i]\) 表示当前处于 \(i\) 状态,循环所有的状态:

  • 每次循环判断其他科目是否已完成,
    如果未完成,将现在状态加上完成的状态记录为新状态。

注意:
因为需要输出路径,所以dp要记录上一个节点:pre
因为要计算答案,所以dp要记录完成这些科目需要多长时间:time
因为要统计数量,所以dp要记录罚时:num

所有科目均为 1 就是答案,也就是\(dp[2^n-1]\)

由于输出是逆序,需要用栈等结构倒过来输出

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1<<16;
// #define int long long
int n;
struct node{
    string name;
    int time;
    int en;
}a[20];
struct DP{
    int num;
    int day;
    int pre;
}dp[N];
int GetChange(int num1,int num2) {
    int n = num1 ^ num2;
    int i = 0;
    while(n) {
        if(n & 1)
            return i;
        n >>= 1;
        i++;
    }
    return 0;
}
void solve(){
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>a[i].name>>a[i].en>>a[i].time;
    }
    memset(dp,0x3f,sizeof dp);  
    dp[0].num = dp[0].day = dp[0].pre =0;

    for(int i=0;i<(1<<n);i++){
        for(int j=0;j<n;j++){
            if((1<<j) & i ) continue;

            int ne=i|(1<<j);
            int cnt=max(0 , dp[i].day+a[j].time-a[j].en);
            // cout<<i<<" "<<j<<" " <<ne<<" "<<cnt<<" "<<"XxX"<<endl;
            if(dp[ne].num>dp[i].num+cnt){
                dp[ne].num=dp[i].num+cnt;
                dp[ne].day=dp[i].day+a[j].time;
                dp[ne].pre=i;
            }
        }
    }
    int k = (1 << n) - 1;
    cout<<dp[k].num<<endl;
    stack<int >  stk;
    while(k){
        int pos = GetChange(k,dp[k].pre);
        stk.push(pos);
        k = dp[k].pre;
    }

    while(!stk.empty()) {
        cout << a[stk.top()].name << endl;
        stk.pop();
    }
}
signed main()
{
     cin.tie(0);
    std::cin.sync_with_stdio(false);
    int t;cin>>t;
    while(t--) solve();

    return 0;
}

棋盘状压DP

原文
AcWing 1064. 小国王【线性状压DP+滚动数组优化+目标状态优化】

题目描述

\(n×n(1≤n≤10)\) 的棋盘上放 \(k(0≤k≤n2)\) 个国王

国王可攻击相邻的 88 个格子,求使它们 无法互相攻击 的 方案总数

八相邻:
通常意义上的八相邻指的是当前元素的上、下、左、右、左上、右上、左下、右下八个方向

分析

这种 棋盘放置类 问题,在没有事先知道一些特定 性质 的情况下来做,都会想到 爆搜

本题的数据规模,也是向着 爆搜 去设置的

如果我们直接 爆搜,则 时间复杂度 为 \(O(2^{n^2})\) 是会超时的,因此会想到用 记忆化搜索 来进行优化

考虑一下如何进行 动态规划

由于在第 \(i\) 层放置国王的行为,受到 \(i−1\) 层和 \(i+1\) 层以及 \(i\) 层的状态影响

那么我们就可以规定从上往下枚举的顺序,这样考虑第 \(i\) 层状态时,只需考虑 \(i−1\) 层的状态即可

于是乎我们可以考虑把层数 \(i\) 作为动态规划的 阶段 进行 线性DP

而第 \(i\) 阶段需要记录的就是前 \(i\) 层放置了的国王数量 \(j\),以及在第 \(i\) 层的 棋盘状态 \(k\)

然后按照题意找到,哪些 棋盘状态 是合法的,哪些 棋盘状态的转移 是合法的即可

image

代码

#include <iostream>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 12, M = 1 << N, C = N * N;

int n, m, K;
LL f[N][C][M];
int cnt[M];
vector<int> legal_state;
vector<int> state_trans[M];

bool check(int state)
{
    return !(state & state >> 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 >> K;
    //预处理所有合法状态
    for (int st = 0; st < 1 << n; ++ st)
        //检查当前状态是否合法
        if (check(st))
            legal_state.push_back(st),
            cnt[st] = count(st);
    m = legal_state.size();
    //预处理所有合法状态的合法转移
    for (auto cur_st: legal_state)
        for (auto to_st: legal_state)
            if (!(cur_st & to_st) && check(cur_st | to_st))//上下不相邻且纵坐标也不相邻
                state_trans[cur_st].push_back(to_st);
    //动态规划
    f[0][0][0] = 1;
    for (int i = 1; i <= n + 1; ++ i)
        for (int j = 0; j <= K; ++ j)
            for (auto &state: legal_state)
                for (auto &pre_st: state_trans[state])
                    if (j - cnt[state] >= 0)
                        f[i][j][state] += f[i - 1][j - cnt[state]][pre_st];
    //统计目标状态的所有方案数
    cout << f[n + 1][K][0] << endl;
    return 0;
}

posted @ 2022-10-03 10:32  kingwzun  阅读(36)  评论(0编辑  收藏  举报