状压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\)
然后按照题意找到,哪些 棋盘状态 是合法的,哪些 棋盘状态的转移 是合法的即可
代码
#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;
}