最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
在 n×n(1≤n≤10) 的棋盘上放 k(0≤k≤n2) 个国王
国王可攻击相邻的 8 个格子,求使它们 无法互相攻击 的 方案总数
八相邻:
通常意义上的八相邻指的是当前元素的上、下、左、右、左上、右上、左下、右下八个方向
分析
这种 棋盘放置类 问题,在没有事先知道一些特定 性质 的情况下来做,都会想到 爆搜
本题的数据规模,也是向着 爆搜 去设置的
如果我们直接 爆搜,则 时间复杂度 为 O(2n2) 是会超时的,因此会想到用 记忆化搜索 来进行优化
考虑一下如何进行 动态规划
由于在第 i 层放置国王的行为,受到 i−1 层和 i+1 层以及 i 层的状态影响
那么我们就可以规定从上往下枚举的顺序,这样考虑第 i 层状态时,只需考虑 i−1 层的状态即可
于是乎我们可以考虑把层数 i 作为动态规划的 阶段 进行 线性DP
而第 i 阶段需要记录的就是前 i 层放置了的国王数量 j,以及在第 i 层的 棋盘状态 k
这里先分析一下,哪些 棋盘状态 是合法的,哪些 棋盘状态的转移 是合法的
合法的棋盘状态
如上图所示,绿色方块为摆放国王的位置,红色方块为王的 攻击范围
只要任意王之间只要 不相邻,就是 合法的状态
合法的棋盘转移状态
如上图所示,绿色方块为摆放国王的位置,红色方块为王的 攻击范围
只要任意王的 纵坐标不相邻,就是 合法的转移状态
我们可以用 二进制 来表示当前的状态
怎么用二进制表示状态?
若(state >> i) == 1
则表示在当前状态中,第 k(0≤i<n) 个位置放置了国王(下标从 0 开始)
再举一个简单的例子,见下图:
用二进制表示该状态,就是 (00100010)b
于是,就有如下 闫氏DP分析
闫氏DP分析法
状态表示—集合fi,j,k: 考虑前 i 层的棋盘,前 i 层放置了 j 个国王,且第 i 层状态是 k 的方案
状态表示—属性fi,j,k: 方案的总数 Sum
状态计算—fi,j,k:
fi,j,k=∑prefi−1,j−cntk,pre
其中pre是枚举的能够与k合法存在于相邻行中的所有状态,cntk表示状态k中的国王数量
初始状态: f0,0,0
目标状态: fn,K,st(其中st为所有合法状态)
这样直接做,时间复杂度是 O(n3K2n2n) 是会超时的
但是我们可以通过预处理所有的 合法状态,以及合法的 相邻转移状态,以及 合法状态 摆放的国王数量
因为虽然状态很多,但是 合法状态 并不多, 合法的转移状态 更不多
Code
#include <iostream>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 11, 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; ++ 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];
//统计目标状态的所有方案数
LL res = 0;
for (auto state: legal_state) res += f[n][K][state];
cout << res << endl;
return 0;
}
滚动数组优化思路
由于第 i 阶段状态只会用到第 i−1 阶段的状态,因此我们可以采用滚动数组来优化空间
只贴上优化的部分
#include <iostream>
int main() //不加这部分他高亮不出来,泪目T_T
{
f[0][0][0] = 1;
for (int i = 1; i <= n; ++ i)
for (int j = 0; j <= K; ++ j)
for (auto &state: legal_state)
{
f[i & 1][j][state] = 0; //要先清空
for (auto &pre_st: state_trans[state])
if (j - cnt[state] >= 0)
f[i & 1][j][state] += f[(i - 1) & 1][j - cnt[state]][pre_st];
}
LL res = 0;
for (auto state: legal_state) res += f[n & 1][K][state];
cout << res << endl;
}
y总的目标状态优化思路
我们可以很轻易的观察到,如果当前层的一个棋子都不摆,即state=(000000)b
那么所有 合法的状态 都是该状态的 合法转移状态
翻译成白话就是:只要这层不摆东西,则上一层 只要合法,那一定可以转移到这一层的这个状态
因此我们可以把目标状态设为 fn+1,K,0
该状态表示 考虑前 n+1 层的棋盘,前 n+1 层放置了 K 个国王,且第 n+1 层什么也没放的方案
根据我们之前提到的 状态转移 可知,该状态会把所有到第 n 层的 合法状态 都转移到自己身上
这样最后我们就 不需要额外枚举所有的目标状态 了
完整代码如下:
#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;
}
彩铅大佬出书吧,写的太好了
😂
+1
自己手动改滚动数组忘了 f[i & 1][j][state] = 0; //要先清空这条,总算可以AC了
为什么要清空滚动数组啊,佬
f[ i&1 ][ j ][ state ]是上上一层使用过的,有数据残留
预处理部分和基础课的蒙德里安的梦想 有些类似
这里的&引用符号 什么作用 juju
大抵是按位与
for (int i = 0; i <= n; i ++ ) f[i][0][0] = 1;
这样初始化我觉得按照定义来看是合理的,但是是错的,有没有大佬能说一下
速速出书
orz
太通透了,膜铅笔佬
大佬,请问一下在dp阶段的这个if条件
(j - cnt[state] >= 0
如果说上一行1的个数加上当前行1的个数大于j怎么办,这里似乎没有判断?
对结果并不影响,因为目标状态是f[n][k][state],就算是转移过程中第二维大于了k,也不可能通过任何路径转移到目标状态(不过判断一下会优化点时间⊙▂⊙)
应该从当前行数看 它是以当前第i行 国王数== j 找到上一行国王数== j-cnt【state】 的数; 且行数逐渐添加,上一行的国王数(f[i - 1][j - cnt[state]][pre_st]),必定存在;
贴个代码
/* dp[i][j][k] 表示第i行状态为j,放了k个大将军 满足l & j ==0 (j << 1) & (2^n - 1) & l == 0; (j >> 1) & l == 0; (无冲突) j和l都是合法方案 dp][i][j][k] += dp[i - 1][l][k - cnt[j]] cnt[j]表示第i行放置的数量 */ #include <bits/stdc++.h> using namespace std; const int N = 100010; int n, m, idx = 0, area[N], cnt[N]; long long dp[15][2020][110], ans; void dfs(int k, int sum, int node) { if (node >= n) { area[++idx] = k; cnt[idx] = sum; return; } dfs(k, sum, node + 1); //不放 dfs(k + (1 << node), sum + 1, node + 2); //放一个 } int main() { scanf("%d%d", &n, &m); dfs(0, 0, 0); for (int i = 1; i <= idx; i++) dp[1][i][cnt[i]] = 1; for (int i = 2; i <= n; i++) for(int j = 1; j <= idx; j++) for (int k = 1; k <= idx; k++) { if (area[j] & area[k]) continue; if ((area[j] << 1) & area[k]) continue; if (area[j] & (area[k] << 1)) continue; for (int s = m; s >= cnt[j]; s--) dp[i][j][s] += dp[i - 1][k][s - cnt[j]]; } for (int i = 1; i <= idx; i++) ans += dp[n][i][m]; printf("%lld", ans); return 0; }
用二进制表示状态那里是不是有点问题?用二进制表示该状态,就是 (00100010)b
二进制的下标顺序从右往左,右边第一位表示0
orz
ORZ
Orz
为什么先清空??
f[i & 1][j][state] = 0; //要先清空
转移用的
+=
,不清空会用到前前阶段的状态爆赞👍
写的很好啊,我先赞一个