刷题周记(九)——#状压DP:最短Hamilton路径、小国王(互不侵犯)、玉米田(Corn Fields G)、愤怒的小鸟、吃奶酪、炮兵阵地、宝藏 #区间DP:清空字符串#DP:关灯问题II
文章目录
- ——2020年12月20日(周日)——————————————————
- 状压DP
- 一、最短Hamilton路径(模板题)
- 二、玉米田(P1879 [USACO06NOV]Corn Fields G)——(棋盘式)
- 三、小国王(P1896 [SCOI2005]互不侵犯)——(棋盘式)
- ——2020年11月21日(周一)——————————————————
- ?设计密码(我也不知道什么时候会做,这周是状压DP)
- ——2020年11月22日(周二)——————————————————
- 一、清空字符串
- 二、愤怒的小鸟(NOIP2016提高组)
- 三、关灯问题II(状压??)
- ——2020年11月18日(周三)——————————————————
- 一、吃奶酪
- 二、炮兵阵地
- ——2020年11月19日(周四)——————————————————
- 一、宝藏
- ——2020年11月20日(周五)——————————————————
- ——2020年11月21日(周六)——————————————————
- ——(完)——————————————————
——2020年12月20日(周日)——————————————————
状压DP
用二进制表示某个 状态 ,然后枚举目标状态,再枚举这个目标状态可以由哪几种状态转移过来,将这几种状态的方案数相加就是当前目标状态的方案数。
一、最短Hamilton路径(模板题)
题目
模板题
理解之后很简单的。
总之就是:
第一维枚举一下当前目标状态(我们想要得到的状态)
第二维枚举一下当前目标状态下,最后一个进来的点
然后枚举一下这个点是与哪个点连起来的…………这不就是图论里Prim算法的原理嘛!
生成一棵最小生成树:
从点1开始建立连通块,
不断地用当前距离最小的点更新其它 连通块之外的 点的距离,
然后将这个点加入连通块。只是这里状态转移的时候会有一些条件限制,而且枚举的方法也不一样。
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
int m[N][N];
int f[1 << N][N];
int main(){memset(f, 0x3f, sizeof f);int n; cin >> n; for(int i = 0; i < n; i ++)for(int j = 0; j < n; j ++)scanf("%d", &m[i][j]);//这里枚举的是当前最后路径状态f[1][0] = 0;for(int i = 1; i < (1 << n); i ++){//这里枚举的是当前最终状态下最后一个进来的点//(还要判断一下当前枚举到得最终状态下是否存在这个点)for(int j = 0; j < n; j ++) if((i >> j) & 1)//这里枚举k点,j点是从k点转移来的。for(int k = 0; k < n; k ++) if((i ^ (1 << j)) >> k & 1)f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + m[k][j]);}cout << f[(1 << n) - 1][n -1];return 0;
}
二、玉米田(P1879 [USACO06NOV]Corn Fields G)——(棋盘式)
题目来源1
题目来源2
输入得时候有个点:
/*得到该行的对应二进制数。
为什么要倒过来储存呢?
因为后面的代码 !(state[j] & w[i]) 是用来判断当前合法状态能否用这一行的玉米田表示出来的,
中间用的符号是 “与(&)”,如果不倒过来用1的话,如果无论这一位上面不需要种有玉米,难道就不能被表示出来吗?
举例,当前这一行玉米田输入的是 1 1 1,1 1 1可以表示0 0 0 这个状态,但是如果用(111&000)显然为0,这就矛盾了。
于是我们反过来记录,并将最后的判断结果再一次反过来,也就是!(000&000)这样就可以得到想要的结果了。
换个说法:
玉米田第i行的输入状态:1 0 1
要表示的状态1:0 0 0
101&000 = 0,不可以被表示,
!(010&001) = 1,可以被表示。
要表示的状态2:0 0 1
101&001 = 1,可以被表示,
!(010&001) = 1,可以被表示。
要表示的状态3:1 0 1
101&101 = 5,可以被表示,
!(010&001) = 1,可以被表示。
那么这一对比就很明显了,这样倒过来就是为了保证要表示000这个合法状态的时候不会出错,
另行判断的话代码会变得很复杂,为了保证代码的思路更加清晰,于是用这种巧妙的办法来规避。
*/
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int w[N];
vector<int> state;
vector<int> head[M];
int f[N][M];//检查是否有连续的1出现
bool check(int state){for (int i = 0; i + 1 < m; i ++ )if ((state >> i & 1) && (state >> i + 1 & 1))return false;return true;
}int main(){cin >> n >> m;for (int i = 1; i <= n; i ++ )for (int j = 0; j < m; j ++ ){int t; cin >> t;w[i] += !t * (1 << j);}//得到所有的合法状态。for (int i = 0; i < 1 << m; i ++ )if (check(i))state.push_back(i);//得到所有与合法状态a相容的合法状态bfor (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))head[i].push_back(j);}f[0][0] = 1;//行数for (int i = 1; i <= n + 1; i ++ )//找第i行的合法状态for (int j = 0; j < state.size(); j ++ )//合法状态j在这一行可以被表示出来。if (!(state[j] & w[i]))//然后找第i - 1行的合法状态for (int k : head[j])//对应合法状态应该是上一行相容的所有合法状态的方案数的总和f[i][j] = (f[i][j] + f[i - 1][k]) % mod;cout << f[n + 1][0] << endl;return 0;
}
以下是二次写的,不知道哪里错了……
#include<bits/stdc++.h>
using namespace std;
const int N = 14;int n, m;
int a[N];
vector<int> state;
vector<int> S[N];
int f[N][1 << N];bool check(int state)
{for (int i = 0; i + 1 < n; i ++ )if (!(state >> i & 1) && !((state >> i + 1) & 1))return false;return true;
}int main(){cin >> n >> m;//土地状态 for(int i = 1; i <= m; i ++)for(int j = 1; j <= n; j ++){int c; cin >> c;a[i] = (a[i] << 1) | c;}int inf = 1 << n - 1;//求所有合法状态 :0代表种,1代表不种 ,左右合法 for (int i = 0; i < 1 << m; i ++ )if (check(i))state.push_back(i);for(int i = 0; i < state.size(); i ++)for(int j = 0; j < state.size(); j ++)//上下合法if((state[i] | state[j]) == inf)S[i].push_back(j);f[0][inf - 1] = 1;//行 for(int k = 1; k <= m + 1; k ++)//当前行的状态 for(int i = 0; i < state.size(); i ++){int mrk = state[i];if( ( mrk | a[k] ) == inf){//上一行的状态for(int j = 0; j < S[i].size(); j ++){int pre_mrk = state[S[i][j]];if(pre_mrk | a[k - 1] == inf)f[k][mrk] += f[k - 1][pre_mrk]; }}}cout << f[m + 1][inf];return 0;
}
三、小国王(P1896 [SCOI2005]互不侵犯)——(棋盘式)
题目来源1:luogu
题目来源2:Acwing
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << 10, K = 110;
int n, m;
//这里是合法状态,由于是用二进制来进行表示,所以一个整数就是一个状态。
vector<int> state;
int cnt[M];
vector<int> head[M];
LL f[N][K][M];//这里检查的是相邻的两位之间有没有都是一得情况
bool check(int state)
{for (int i = 0; i < n; i ++ )if ((state >> i & 1) && (state >> i + 1 & 1))return false;return true;
}//这里数的是当前合法状态下有多少个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 >> m;//这里是初始化,先将所有的合法状态压缩成一个数,然后入队state,并数出里面有多少个1for (int i = 0; i < 1 << n; i ++ )if (check(i)){state.push_back(i);cnt[i] = count(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))head[i].push_back(j);}f[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 ++ )//然后找到与a相容的合法状态bfor (int b : head[a]){//记住a是下标,cnt记录的元素是状态。int c = cnt[state[a]];//当然,这个数量不可以超过枚举到的国王数。if (j >= c) //当前状态应该是由所有上一行的状态为b,且放了j - c个棋子的方案的总和。f[i][j][a] += f[i - 1][j - c][b];}//最后输出答案cout << f[n + 1][m][0] << endl;return 0;
}
——2020年11月21日(周一)——————————————————
?设计密码(我也不知道什么时候会做,这周是状压DP)
题目
——2020年11月22日(周二)——————————————————
一、清空字符串
莫名其妙又又没有保存,写了半天气死…………
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 610;
char s[N] , s1[N];
int n , ans , len;
int dp[N][N];
int main()
{memset(dp , 0x3f , sizeof dp);scanf("%d" , &len);scanf("%s" , s1 + 1);for(int i = 1 ; i <= len ;){int j = i + 1;while(s1[i] == s1[j] && j <= len) j++;s[++n] = s1[i];i = j;}for(int i = 1 ; i <= n ; i++) dp[i][i] = 1;//得到新的字符串并初始化 for(int k = 1 ; k <= n ; k++){for(int l = 1 ; l + k <= n ; l++){int r = l + k;//这里先忽略两头的字符,因为最优解肯定是从中间开始合并的,那么就留着两头中的一个最后解决,因为最后肯定只剩下头和尾了。dp[l][r] = min(dp[l + 1][r] , dp[l][r - 1]);for(int j = l + 1 ; j < r ; j++)dp[l][r] = min(dp[l][r] , dp[l + 1][j] + dp[j + 1][r]);for(int j = l ; j < r - 1 ; j++)dp[l][r] = min(dp[l][r] , dp[l][j] + dp[j + 1][r - 1]);if(s[l] != s[r])dp[l][r]++;
// cout << dp[l][r] << " ";}
// cout << endl;}//谁也不知道为什么这里还要再跑一遍找最优答案,不过就是有一个点过不了,郁闷。for(int j = 1 ; j < n ; j++)dp[1][n] = min(dp[1][n] , dp[1][j] + dp[j + 1][n]);int res = dp[1][n];printf("%d" , res);return 0;
}
二、愤怒的小鸟(NOIP2016提高组)
的确够恶心,将抛物线和状压DP结合,题目里面还给了一个没用的变量m(居然还一本正经地解释了一大堆奇怪的用途)就更离谱了。
下面这张图是得到一个 过两个定点的抛物线 的方程式推导。
随便选一个猪猪进行打击那一步实在麻烦,结合某个例子来理解会更清晰。
注释版代码:
这个是允许的误差值,因为是浮点数double,所以无法像整数那样直接判断相等。
const double eps = 1e-8;输入的小猪位置
PDD q[N];这里是“可以打到第i个小猪和第j个小猪的抛物线可以打到的小猪”的状态:1表示这个小猪打爆了,0表示这个小猪还活得好好的。
int path[N][N];这里表示满足 打击后的状态变成M 的情况下需要的最少抛物线数。
int f[M];这里就是double的按精度比较大小的办法
int cmp(double x, double y)
{if (fabs(x - y) < eps) return 0;if (x < y) return -1;return 1;
}for (int i = 0; i < n; i ++ ){只打到一个小猪的状态就是只有这一位上为1。path[i][i] = 1 << i;for (int j = 0; j < n; j ++ ){将两个小猪的横纵坐标提出来double x1 = q[i].x, y1 = q[i].y;double x2 = q[j].x, y2 = q[j].y;不是同一条直线上的才可以进行下一步if (!cmp(x1, x2)) continue;这个可以找到一个同时穿过两个点的抛物线,绝对可以的,试着把二次函数图象变得夸张点就会明白了。double a = (y1 / x1 - y2 / x2) / (x1 - x2);double b = y1 / x1 - a * x1;a接近0的时候是不合法的,-b/2a中分母不能为0;if (cmp(a, 0) >= 0) continue;state就是状态了int state = 0;然后枚举一下这个抛物线可以又穿过哪些点。for (int k = 0; k < n; k ++ ){double x = q[k].x, y = q[k].y;要是x代入后得到的y于这个点的y相等的话,这个状态上对应的位置就可以变为1;if (!cmp(a * x * x + b * x, y)) state += 1 << k;}然后将“可以打到第i个小猪和第j个小猪的抛物线可以打到的小猪“这个状态记录一下。path[i][j] = state;}}memset(f, 0x3f, sizeof f);f[0] = 0;枚举2^n - 2种状态因为全是1的情况不用更新了。for (int i = 0; i + 1 < 1 << n; i ++ ){int x = 0;枚举第j位上的数,找到第一个0,也就是找到第一个还没有被打到的小猪,为什么呢?因为我们要打爆它啊。其实等于是随便找一个0来进行打击啦,这样的效果最后是和全部0都打一遍是一样的。举例子:假设有三个猪猪,三个猪猪做不到同时打击,但是有每两个我们都可以有办法打到.首先是将所有的状态枚举出来。path[1][1] = 001; path[1][2] = 011; path[1][3] = 101;path[2][1] = 011; path[2][2] = 010; path[2][3] = 110;path[3][1] = 101; path[3][2] = 110; path[3][3] = 100;f[000] = 0: f[001] = 1, f[011] = 1, f[101] = 1;其实到下面这步就已经搞定了,后面都没必要了都。不过这是数据问题,其它数据不一定能过,这只能说明这个方法实际操作十分可行。f[001] = 1: f[011] = 2, f[011] = 2, f[111] = 2;有些状态时不会被更新到的,不难看出也没有更新的必要,也没有用它来更新其它状态的必要。f[010] = 0x3f: f[011] = 1, f[011] = 1, f[111] = 2;f[011] = 1: f[111] = 2 , f[111] = 2, f[111] = 2;f[100] = 0x3f: f[101] = 1, f[111] = 2, f[101] = 1;f[101] = 1: f[111] = 2, f[111] = 2, f[111] = 2;f[111] = 2就没必要再搞了,因为已经搞定了。不难发现,其中会有一些值没有被更新,因为有它没它都是一样的答案,没它的话反而时间上会减少,岂不美哉?不死心的话再看看全部遍历一遍的结果:……以后再补上吧,浪费时间,将上面的手模一遍差不多了其实最主要的原因是,这条抛物线上有可能有其它的点,有时两个点之间往往有很多个点。二进制状态间进行或运算的时候可以一次搞定很多个点,于是不必要一个个地来枚举进行状态转移。这是二进制状态转移的优势,有些状态可以很简单地不漏地就表示出来。for (int j = 0; j < n; j ++ )if (!(i >> j & 1)){x = j;break;}枚举第j位上的数,for (int j = 0; j < n; j ++ )path[x][j]表示“能打到第x个小猪以及第j个小猪的抛物线”f[i | path[x][j]]表示达到两条抛物线组合起来后状态,需要的最少抛物线数。最后还要和f[i]+1也就是原来状态自己随便加上一条抛物线的数量比较一下。f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);}
纯净版代码:
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>#define x first
#define y secondusing namespace std;typedef pair<double, double> PDD;const int N = 18, M = 1 << 18;
const double eps = 1e-8;int n, m;
PDD q[N];
int path[N][N];
int f[M];int cmp(double x, double y)
{if (fabs(x - y) < eps) return 0;if (x < y) return -1;return 1;
}int main()
{int T; cin >> T;while (T -- ){cin >> n >> m;for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y;memset(path, 0, sizeof path);for (int i = 0; i < n; i ++ ){path[i][i] = 1 << i;for (int j = 0; j < n; j ++ ){double x1 = q[i].x, y1 = q[i].y;double x2 = q[j].x, y2 = q[j].y;if (!cmp(x1, x2)) continue;double a = (y1 / x1 - y2 / x2) / (x1 - x2);double b = y1 / x1 - a * x1;if (cmp(a, 0) >= 0) continue;int state = 0;for (int k = 0; k < n; k ++ ){double x = q[k].x, y = q[k].y;if (!cmp(a * x * x + b * x, y)) state += 1 << k;}path[i][j] = state;}}memset(f, 0x3f, sizeof f);f[0] = 0;for (int i = 0; i + 1 < 1 << n; i ++ ){int x = 0;for (int j = 0; j < n; j ++ )if (!(i >> j & 1)){x = j;break;}for (int j = 0; j < n; j ++ )f[i | path[x][j]] = min(f[i | path[x][j]], f[i] + 1);}cout << f[(1 << n) - 1] << endl;}return 0;
}
三、关灯问题II(状压??)
题目
经典状压问题,感觉还好。
好像不能用状压来做……
好吧,的确有问题
以下是一开始的有问题的分析
for( i = (1<<n)-1;i>=0;i--){//枚举状态for( j = 1; j <= m; j ++){//枚举开关int now = i;//我们的now最终会变为,按完这个开关后的状态.for( l = 1;l <= n;l ++){//枚举控制的灯.if(a[j][l] == 0)continue;//不操作//以下是还原到上一步的状态。 if(a[j][l] == 1 and (i & (1<<(l-1)))) now ^= (1<<(l-1));//第一个操作, 关灯if(a[j][l] == -1 and !(i & (1<<(l-1)))) now ^= (1<<(l-1));//第二个操作,开灯}f[now] = min(f[now], f[i]+1);//常规操作,求最小操作次数.//因为我们可以通过i状态操作按一个开关到达now状态.//所以是f[i]+1.}}
纯净版代码(有问题的)
#include<bits/stdc++.h>
using namespace std;
const int N = 11, M = 1000;
int n, m, f[1 << N], a[M][N];
int main() {scanf("%d%d", &n, &m);for(int i = 1; i <= m; ++i) for(int j = 1; j <= n; ++j) scanf("%d", &a[i][j]);memset(f, 0x3f, sizeof f);f[0] = 0;for(int i = 0; i < (1 << n); ++i) {for(int k = 1; k <= m; ++k) {int s = i;for(int j = 0; j < n; ++j)if((a[k][j + 1] == 1 && !(i & 1 << j)) || (a[k][j + 1] == -1 && i & 1 << j))s ^= 1 << j;f[s] = min(f[s], f[i] + 1); } }if(f[(1 << n) - 1] == 0x3f3f3f3f)puts("-1");elseprintf("%d\n",f[(1 << n) - 1]);return 0;
}
用状压来做会有些数据无法过去(但是洛谷还是给AC就离谱),比如:
3
3
1 -1 1
0 1 -1
0 0 1
用程序出来的答案是-1,但是我们手模一遍:
分析数据:
假如按下1,对A来说会关上,对B来说会打开,对C来说会关上,即强制变成:010
假如按下2,对A来说会不变,对B来说会关上,对C来说会打开,即强制变成:?01
假如按下3,对A来说会不变,对B来说会不变,对C来说会关上,即强制变成:??0 (?代表不管)
一开始状态为:1 1 1
按下第1个开关变成: 0 1 0
按下第2个开关变成: 0 0 1
按下第3个开关变成: 0 0 0
再来用我们写的程序手模一遍……
这是调试程序,写的什么不重要。
#include<bits/stdc++.h>
using namespace std;
const int N = 11, M = 1000;
int n, m, f[1 << N], a[M][N];void out(int s){for(int i = 0; i < 3; i ++)printf("%d", (s >> i) & 1);
}int main(){freopen("t.in", "r", stdin);freopen("t.out", "w", stdout);scanf("%d%d", &n, &m);for(int i = 1; i <= m; ++i) for(int j = 1; j <= n; ++j) scanf("%d", &a[i][j]);memset(f, 0x3f, sizeof f);f[0] = 0;//当前状态 for(int i = 0; i < (1 << n); ++i){//开关数 bool ou = 1;for(int k = 1; k <= m; ++k) {//得到当前状态,以便还原状态时候用 int s = i;//第j位上的灯 for(int j = 0; j < n; ++j)//对应效果和对应状态 if((a[k][j + 1] == 1 && !(i & (1 << j))) || (a[k][j + 1] == -1 && i & (1 << j)))s ^= 1 << j; f[s] = min(f[s], f[i] + 1);if(s != i){out(s);printf(" %d\n", f[s]); ou = 0;}}if(ou){out(i);cout << endl;} }if(f[(1 << n) - 1] == 0x3f3f3f3f)puts("-1");elseprintf("%d\n",f[(1 << n) - 1]);return 0;
}
这是结果
101 1
010 1
001 1
101 1
110 1061109567
101 1
101 1
010 1
011 2
101 1
110 1061109567
111 1061109567
101 1
010 1
001 1
101 1
110 2
101 1
101 1
010 1
011 2
101 1
110 2
111 1061109567
-1
可以看到,111这个状态最后是没有被任何状态更新的
也就是说,我们这个状态转移是错误的。(据说是有后效性没有处理,但我个人理解为这个转移本身就不符合题意)
以下是最终的写法(不是状压DP)
#include<bits/stdc++.h>
using namespace std;
int f[2][1 << 11];
int a[100][10];
int n, m;
//当前状态,开关序号
int work(int now, int k){for(int i = 0; i < n; i ++){//如果是操作1并且当前位置是1 if(a[k][i] == 1 && (now >> i) & 1) now ^= (1 << i);//如果是操作-1并且当前位置是0 if(a[k][i] == -1 && !((now >> i) & 1)) now ^= (1 << i);}return now;
}int main(){cin >> n >> m;for(int i = 1; i <= m; i ++)for(int j = 0; j < n; j ++)cin >> a[i][j];int inf = (1 << n) - 1;f[0][inf] = 1;int now_h = 0;//从第一行开始不断地往下走 (滚动数组)for(int i = 0; i < m * m; i ++){int nxt_h = 1 - now_h;//一定要记得清空下一行!!!memset(f[nxt_h], 0, sizeof f[nxt_h]);//然后是枚举当前行的状态j可以是什么 for(int j = inf; j > 0; j --){if(!f[now_h][j]) continue;
// //然后将所有可以走到的下一个状态变成1
// //这里枚举开关 for(int k = 1; k <= m; k ++)f[nxt_h][work(j, k)] = 1; }//如果在当前这一步就已经完成了最终状态,那就结束吧; if(f[nxt_h][0]){printf("%d", i + 1);return 0;}now_h = nxt_h;}cout << "-1" << endl;return 0;
}
——2020年11月18日(周三)——————————————————
一、吃奶酪
题目
和哈密顿最短路径很像,转移方程也是一模一样。
注释版代码
#include<bits/stdc++.h>
using namespace std;
const int N = 16, M = 1 << 16;
double f[M][N];
double x[N], y[N];double dist(int i, int j){return sqrt( (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) );
}int main(){memset(f, 127, sizeof f);int n; cin >> n;for(int i = 0; i < n; i ++) cin >> x[i] >> y[i];//记得初始化每一个点到原点的距离for(int i = 0; i < n; i ++) f[1 << i][i] = sqrt(x[i] * x[i] + y[i] * y[i]);//状态 for(int i = 1; i < (1 << n); i ++)//找到当前状态下所有为1的位置作为新加入的点for(int j = 0; j < n; j ++) if((i >> j) & 1){//把这一位去掉1(还原状态)后,再枚举有哪些点可以做接点//调了半天居然是运算顺序出了问题,果然不熟悉的运算括号不能省。for(int k = 0; k < n; k ++) if(((i ^ (1 << j)) >> k) & 1)f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + dist(k, j));}double ans = 1e6;for(int i = 0; i < n; i ++) ans = min(ans, f[(1 << n) - 1][i]);printf("%.2lf", ans);return 0;
}
//这里用来调试看数组状态的
// for(int i = 0; i < n; i ++){// for(int j = 0; j < n; j ++)
// cout << dist[i][j] << " ";// cout << endl;
// }
// for(int i = 0; i < 1 << n; i ++) cout << f[i] << " ";
纯净版代码
#include<bits/stdc++.h>
using namespace std;
const int N = 16, M = 1 << 16;
double f[M][N];
double x[N], y[N];double dist(int i, int j){return sqrt( (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) );
}int main(){memset(f, 127, sizeof f);int n; cin >> n;for(int i = 0; i < n; i ++) cin >> x[i] >> y[i];for(int i = 0; i < n; i ++) f[1 << i][i] = sqrt(x[i] * x[i] + y[i] * y[i]);for(int i = 1; i < (1 << n); i ++)for(int j = 0; j < n; j ++) if((i >> j) & 1){for(int k = 0; k < n; k ++) if(((i ^ (1 << j)) >> k) & 1)f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + dist(k, j));}double ans = 1e6;for(int i = 0; i < n; i ++) ans = min(ans, f[(1 << n) - 1][i]);printf("%.2lf", ans);return 0;
}
二、炮兵阵地
题目
注释版代码
#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = 10, S = 1 << M;
int n, m;
int g[N];
//行数,上一行,本行
int f[2][S][S];
vector<int> state;
int cnt[S];//检查本行内部是否合法
bool check(int s){for(int i = 0; i < m; i ++)//如果s >> (i + 1) & 1中不加括号,也是先算加法再进行位运算。if((s >> i & 1) && ((s >> i + 1 & 1) || (s >> i + 2 & 1)))return false;return true;
}//找这个状态里面有多少个1
int count(int s){int res = 0;while(s){res += s & 1;s >>= 1;}return res;
}int main(){cin >> n >> m;for(int i = 0; i < n; i ++)for(int j = 0; j < m; j ++){char c; cin >> c;//1是山地,0是平原,表示可以塞一个炮兵。if(c == 'H') g[i] += 1 << j;}//为state寻找所有本行内部合法的状态。for(int i = 0; i < 1 << m; i ++)if(check(i)){state.push_back(i);//顺便找找当前合法状态里面有多少个1cnt[i] = count(i);}//行数,最后可以表示摆到到n + 1行并且第n + 1行和第n行都一个也没有放的最多数量;//其实就是恰好摆到第n - 1行的最大数量。for(int i = 0; i < n + 2; i ++)for(int u = 0; u < state.size(); u ++)for(int j = 0; j < state.size(); j ++)for(int k = 0; k < state.size(); k ++){int a = state[u], b = state[j], c = state[k];//首先是炮兵之间没有交集,以免他们炸到自己的炮友(同一个炮兵部队的战友)。if((a & b) || (a & c) || (b & c)) continue;//并且本行炮兵没有被安排在山上,扛着大炮上山什么的炮兵可不干。if(g[i] & c) continue;//然后就是滚动数组//与上一行的对应合法状态的各种合法状态相比f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[c]);}/*这三行枚举可以随便换位置,本人亲测,没有影响。这里我就按照下面的顺序来写了。u:第i - 2行j:第i - 1行k:本行*/cout << f[n + 1 & 1][0][0] << endl;return 0;
}
纯净版代码
#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = 10, S = 1 << M;
int n, m;
int g[N];
int f[2][S][S];
vector<int> state;
int cnt[S];bool check(int s){for(int i = 0; i < m; i ++)if((s >> i & 1) && ((s >> i + 1 & 1) || (s >> i + 2 & 1)))return false;return true;
}int count(int s){int res = 0;while(s)res += s & 1,s >>= 1;return res;
}int main(){cin >> n >> m;for(int i = 0; i < n; i ++)for(int j = 0; j < m; j ++){char c; cin >> c;if(c == 'H') g[i] += 1 << j;}for(int i = 0; i < 1 << m; i ++)if(check(i)){state.push_back(i);cnt[i] = count(i);}for(int i = 0; i < n + 2; i ++)for(int u = 0; u < state.size(); u ++)for(int j = 0; j < state.size(); j ++)for(int k = 0; k < state.size(); k ++){int a = state[u], b = state[j], c = state[k];if((a & b) || (a & c) || (b & c)) continue;if(g[i] & c) continue;f[i & 1][j][k] = max(f[i & 1][j][k], f[i - 1 & 1][u][j] + cnt[c]);}cout << f[n + 1 & 1][0][0] << endl;return 0;
}
——2020年11月19日(周四)——————————————————
一、宝藏
题目
学习自这篇博客及ACwing.
注释版代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << 12, INF = 0x3f3f3f3f;
int n, m;
//距离数组
int d[N][N];
//第j行是状态i的情况下的最大价值
int f[M][N], g[M];int main()
{scanf("%d%d", &n, &m);//一开始全是无穷大,表示没有通路memset(d, 0x3f, sizeof d);//然后就是自己到自己的通路长度为0for (int i = 0; i < n; i ++ ) d[i][i] = 0;//对输入的距离进行处理。while (m -- ){int a, b, c;scanf("%d%d%d", &a, &b, &c);//为了方便起见我们的数从0开始算起。a --, b --;//取最小值。d[a][b] = d[b][a] = min(d[a][b], c);}//然后枚举每一种状态,for (int i = 1; i < 1 << n; i ++ )//找到这个状态的每一位数for (int j = 0; j < n; j ++ )//如果第j位是1if (i >> j & 1)//再找另外一个点for (int k = 0; k < n; k ++ )//如果两点之间可以开辟一条通路if (d[j][k] != INF)//那么就找到所有可以拓展到的点。g[i] |= 1 << k;memset(f, 0x3f, sizeof f);//将所有免费的通道先初始化//表示的是只打通到第1层(只有一个根节点)的时候的最大价值for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;//枚举当前更新到的状态for (int i = 1; i < 1 << n; i ++ )/*i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。枚举子集i: 10101j: 10100 10011 10010 10001 10000 01111显然,j & i后得到的一定是i的子集,然后我们还要加上一个条件判断是否可以转移到i;当然*/for (int j = (i - 1) & i; j; j = (j - 1) & i)//栗子:i:1011 j:1001 g[j] = 1111,也就是说,j可以拓展到1011。//i ^ j: 1011 ^ 1001 = 0010if ((g[j] & i) == i){//由于j是i的真子集,所以异或之后得到的1一定是i为1,j为0,也就是j转移得到i,相同的不必转移。int remain = i ^ j; int cost = 0;//找到remain中所有为1的点,也就是j中所有不同于i的点,这是我们要进行转移的位置for (int k = 0; k < n; k ++ ) if (remain >> k & 1){int t = INF;//t表示将k这个点接进来的最小花费//找到j(上一个状态)中所有为1的点作为接点for (int u = 0; u < n; u ++ ) if (j >> u & 1)//找到k与所有接点之间最小距离(接进来的最小花费)t = min(t, d[k][u]);// cost代表将所有k点加上的总花费,也就是将上一状态补足节点到当前状态的总花费。cost += t;}//k是高度for (int k = 1; k < n; k ++ ) //因为是经过的宝藏屋的数量,所以总花费乘以层数就可以了。f[i][k] = min(f[i][k], f[j][k - 1] + cost * k);}//结果就在所有层上状态是1 << n) - 1的结果之间int res = INF;for (int i = 0; i < n; i ++ ) res = min(res, f[(1 << n) - 1][i]);printf("%d\n", res);return 0;
}
/*状态f[i][j]表示:
集合:所有包含i中所有点,且树的高度等于j的生成树
属性:最小花费
状态计算:枚举i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。
那么我们就要知道S中第j - 1层有多少个点
核心: 求出第j层的所有点到S的最短边,将这些边权和乘以j,直接加到f[S][j - 1]上,即可求出f[i][j]。
*/
纯净版代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << 12, INF = 0x3f3f3f3f;
int n, m;
int d[N][N];
int f[M][N], g[M];int main()
{scanf("%d%d", &n, &m);memset(d, 0x3f, sizeof d);for (int i = 0; i < n; i ++ ) d[i][i] = 0;while (m -- ){int a, b, c;scanf("%d%d%d", &a, &b, &c);a --, b --;d[a][b] = d[b][a] = min(d[a][b], c);}for (int i = 1; i < 1 << n; i ++ )for (int j = 0; j < n; j ++ )if (i >> j & 1)for (int k = 0; k < n; k ++ )if (d[j][k] != INF)g[i] |= 1 << k;memset(f, 0x3f, sizeof f);for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;//枚举当前更新到的状态for (int i = 1; i < 1 << n; i ++ )for (int j = (i - 1) & i; j; j = (j - 1) & i)if ((g[j] & i) == i){int remain = i ^ j; int cost = 0;for (int k = 0; k < n; k ++ ) if (remain >> k & 1){int t = INF;for (int u = 0; u < n; u ++ ) if (j >> u & 1)t = min(t, d[k][u]);cost += t;}for (int k = 1; k < n; k ++ ) f[i][k] = min(f[i][k], f[j][k - 1] + cost * k);}int res = INF;for (int i = 0; i < n; i ++ ) res = min(res, f[(1 << n) - 1][i]);printf("%d\n", res);return 0;
}
——2020年11月20日(周五)——————————————————
——2020年11月21日(周六)——————————————————
——(完)——————————————————
刷题周记(九)——#状压DP:最短Hamilton路径、小国王(互不侵犯)、玉米田(Corn Fields G)、愤怒的小鸟、吃奶酪、炮兵阵地、宝藏 #区间DP:清空字符串#DP:关灯问题II相关推荐
- 洛谷P1879 [USACO06NOV]玉米田Corn Fields【状压dp】
P1879 [USACO06NOV]玉米田Corn Fields 时间限制 1.00s 内存限制 125.00MB 题目描述 Farmer John has purchased a lush new ...
- [状压dp] 最短Hamilton路径(模板题+状压dp)
文章目录 0. 前言 1. 状压dp 模板题 0. 前言 状压 dp 就是采用二进制数保存状态,方便进行位运算操作.例如 八皇后.八数码问题也都是采用了状态压缩的思想来使用一个二进制数唯一对应集合中的 ...
- [USACO06NOV]玉米田Corn Fields (状压$dp$)
题目链接 Solution 状压 \(dp\) . \(f[i][j][k]\) 代表前 \(i\) 列中 , 已经安置 \(j\) 块草皮,且最后一位状态为 \(k\) . 同时多记录一个每一列中的 ...
- jzoj1266,P1879-[USACO06NOV]玉米田Corn Fields【状态压缩,dp】
正题 评测记录:https://www.luogu.org/recordnew/lists?uid=52918&pid=P1879 大意 有n*m的矩阵,有些地方可以放,有些不可以放,不可以相 ...
- 最短Hamilton路径(哈密顿图,状压dp)
题目: 给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径. Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次. ...
- 【每日DP】day2、P1879 [USACO06NOV]Corn Fields G玉米地(状压DP模板题)难度⭐⭐⭐★
昨天的每日DP我还在写01背包,今天就到状压DP了,真刺激. P1879 [USACO06NOV]Corn Fields G 题目链接 输入 2 3 1 1 1 0 1 0 输出 9 一道简单的状压D ...
- 最短Hamilton路径(状压dp)
链接:https://ac.nowcoder.com/acm/problem/50909 来源:牛客网 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 262144K,其他语言5242 ...
- 糖果(2019第十届蓝桥杯省赛C++A组I题) 解题报告(状压dp) Apare_xzc
糖果(2019第十届蓝桥杯省赛C++A组I题) 解题报告(状压dp) xzc 2019/4/5 试题 I: 糖果 时间限制: 1.0s 内存限制: 256.0MB 本题总分:25分 [问题描述] ...
- 最短Hamilton路径-状压dp解法
最短Hamilton路径 时间限制: 2 Sec 内存限制: 128 MB 题目描述 给定一张 n(n≤20) 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamil ...
最新文章
- 卷毛机器人抢大龙视频_腾讯这一措施,又砸碎了一些小企业的饭碗,QQ机器人成为历史...
- 【js细节剖析】通过=操作符为对象添加新属性时,结果会受到原型链上的同名属性影响...
- 删除Kali Linux多余的系统架构
- 实现DFS之“农田灌溉”
- 配置nginx的那些参数
- vs里面mfc是什么_最近!一大批人正在前往文安,究竟发生了什么?
- jsp mysql做登入界面_用jsp实现网站登录界面的制作,并连接数据库
- 常见的浏览器兼容性问题大汇总
- 在虚拟机里安装centos 6.4和centos 5.8里配置vim 7.4安装过程
- python重写和重载的区别_Java 重写(Override)与重载(Overload)
- 网站用户的生命周期价值
- 20170825阿里在线笔试之菜鸟仓库货架格子编号
- 《微型计算机原理及应用》复习整理(针对考点)
- 用户登陆问题,session.invalidate销毁session
- Zeppelin解释器的REST API接口
- (Xposed)编写第一个Xposed模块
- 廖雪峰python3高阶函数部分理解
- Mathworks MATLAB for Mac (强大的商业数学软件) v9.11
- Azure RTOS ThreadX 的功能组件
- 天刀手游服务器注册不了,天涯明月刀手游开服常见问题汇总 天涯明月刀手游10月16日开服...