一、小国王
题目链接
图解:
#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总的代码的注释