SWUST状态压缩入门 解题报告
来源:互联网 发布:37轩辕剑入魔进阶数据 编辑:程序博客网 时间:2024/04/29 10:14
【序】
由于昨天大佬讲课,很多人没来,在做完这个训练,我希望用我自己的粗浅理解能够帮到大家,还有训练已经过去几天了,也希望大家抓紧时间去完成这个专题训练,不然这个专题就开得毫无意义了。然后下面的题解我会细致的讲解题目的解法,以及我是如何理解这道题,争取大家都可以很快的看懂。
【正文】 题目传送门:点击打开链接
【A】 题意为给了1个H*W的方格矩阵,然后要我们用1*2小矩形去填满这个大矩形,最后要问填满这个大矩形的方法总数。解题方法:数据范围非常小,我们大概确定这个题可以状压了。我们用2进制的0,1表示一个格子是否放了。我们发现第i行的状态只会和第i-1行有关,所以我枚举 i-1行的每个状态,推出由此状态可以达到的第i行状态,如果i-1行的出发状态某处没有放,必然要在第i行放一个竖的方块,所以这里对上一行的状态按位取反之后的状态机就是放了 竖方块的状态。然后我们DFS扫一道在i行横着的方块的所有可能,并且把这些状态累加上i-1的出发状态的方法数就可以了。下面来个例子:
2 411111111状态可以由1100 0000 0110 0011 11110000 0000 0000 0000 0000这五种i-1的状态达到,故2 4 的答案为5
代码如下:
int n, m;long long dp[12][1<<11], pre; //dp[i][j]第i行状态为jvoid dfs(int r, int sta, int pos)//行r,当前状态sta,当前位置pos{ if(pos >= m){ dp[r][sta] += pre;//i-1行的出发状态的方案数 return ; } dfs(r, sta, pos + 1); if(pos <= m - 2 && !(sta & 1 << pos) && !(sta & 1 << pos+1)){ dfs(r, sta | 1<<pos | 1<<pos + 1, pos + 2); }}int main(){ while(scanf("%d%d",&n,&m) != EOF) { if(n == 0 && m == 0) break; memset(dp, 0, sizeof(dp)); pre = 1; dfs(1, 0, 0); for(int i = 2; i <= n; i++){ for(int j = 0; j < 1<<m; j++){ pre = dp[i-1][j]; dfs(i, ~j&((1<<m) - 1), 0); //先和(1<<(m)-1)相与,保证只有m位二进制 } } printf("%lld\n", dp[n][(1<<m)-1]); } return 0;}
【B】题意为一个N*M的矩阵,其中N<=150,M<=10,矩阵中有K个坏点,芯片不能放在上面,现在要用一些2*3的芯片来填充这个矩阵,问最多可以使用用多少个这样的芯片?解题方法:由于芯片大小是2*3的,意思就是当前行的状态不仅和上一行有关并且也和上上行相关?那么这个状态用什么来表示比较好呢?2进制?可以,但是想想二进制在这里 的判断感觉要写个几十排,不太好。那么3进制呢?下面我们假设用三进制表示,来试一下状态合法性的判断会不会很简单。
由于芯片长度只有 3, 所以第 x 行芯片的放置只受 x-1行 和 x-2 行放置情况的影响。
同时由于一旦 方格 (x-1, y)被黑色记号或其他芯片 占据,则方格(x-2,y)即便空闲对第 x行芯片的放置也毫无意义。
所以,只需记录的状态只有:
一, a[x] = 0 表示方格(x-1,y)空闲,方格(x-2,y)空闲
二, a[x] = 1 表示方格(x-1,y)空闲,方格(x-2,y)被占据
三, a[x] = 3, 表示方格(x-1,y)被占据
意味着对于 I 层, 其状态 J, 保存着 I,I-1 层 芯片的放置方法,
所以我们通过 dp(I,J)更新 I+1 层时,同样要更新 I+1 的状态信息 K, 其保存着 I+1, I层信息
状态 dp(i,j)表示 前 i 层 状态为 J 的最大芯片数量
情况一, 当前列y 不放,从 y+1 列开始考虑
情况二, 当前两列 ( y, y+1 ) 合法,放 3*2 芯片, 更新 I+1层状态 , 再去考虑 y+2列
情况三, 当前三列(y,y+1,y+2)合法,放 2*3 芯片, 更新 I+1 层状态,再去考虑 y+3列
然后我们再来计算一下空间复杂度,3^10 * 150 毫无疑问的炸掉了,然后这里当然就考虑用滚动数组来优化空间了。另,处理列的组合问题需要 递归DFS,通过回溯来解决。 另外,对于黑点,我们其实可以合并到 芯片占据的类型中去,而不需要单独处理,这样就能同过 I层的放置序列 P(p1,p2,...,pm),I+1层的放置序列Q(Q1,Q2,。。,Qm),来决策了。
int vis[155][11];int dp[2][maxn], A[11], P[11], Q[11];int n, m, K, mask;int bianma(int f[]){ //编码 int res = 0; for(int i = 1; i <= m; i++){ res += (f[i] * A[i - 1]); } return res;}void jiema(int x, int f[]){ //解码 for(int i = 1; i <= m; i++){ f[i] = x % 3; x /= 3; }}void dfs(int i, int x, int num) //第i行,x列,数量为num{ if(x > m) return; int cur = (i + 1) & 1, nxt = i & 1, k = bianma(Q); dp[nxt][k] = max(dp[nxt][k], dp[cur][bianma(P)]); if((x <= (m - 1)) && ((P[x] == 0) && (P[x + 1] == 0)) && ((Q[x] == 0) && (Q[x + 1] == 0))){ Q[x] = Q[x + 1] = 2; int kk = bianma(Q); dp[nxt][kk] = max(dp[nxt][kk], num + 1); dfs(i, x + 2, num + 1); Q[x] = Q[x + 1] = 0; } if((x <= (m - 2)) && (P[x] <= 1) && (P[x + 1]) <= 1 && P[x + 2] <= 1 && Q[x] == 0 &&Q[x + 1] == 0 && Q[x + 2] == 0){ Q[x] = Q[x + 1] = Q[x + 2] = 2; int kk = bianma(Q); dp[nxt][kk] = max(dp[nxt][kk], num + 1); dfs(i, x + 3, num + 1); Q[x] = Q[x + 1] = Q[x + 2] = 0; } dfs(i, x + 1, num);}int main(){ A[0] = 1; for(int i = 1; i <= 10; i++) A[i] = A[i - 1] * 3; int T; scanf("%d", &T); while(T--){ scanf("%d%d%d", &n, &m, &K); memset(vis, 0, sizeof(vis)); for(int i = 0; i < K; i++){ int x, y; scanf("%d%d", &x, &y); vis[x][y] = 1; } memset(dp, -1, sizeof(dp)); memset(P, 0, sizeof(P)); memset(Q, 0, sizeof(Q)); for(int i = 1; i <= m; i++){ P[i] = vis[1][i] + 1; } mask = A[m]; dp[1][bianma(P)] = 0; for(int i = 2; i <= n; i++){ int cur = (i + 1) & 1, nxt = i & 1; memset(dp[nxt], -1, sizeof(dp[nxt])); for(int j = 0; j < mask; j++){ if(dp[cur][j] == -1) continue; jiema(j, P); for(int x = 1; x <= m; x++){ if(vis[i][x]) Q[x] = 2; else{ Q[x] = P[x] - 1; if(Q[x] < 0) Q[x] = 0; } } dfs(i, 1, dp[cur][j]); } } int ans = 0; for(int i = 0; i < mask; i++){ ans = max(ans, max(dp[0][i], dp[1][i])); } printf("%d\n", ans); } return 0;}
【C】题意为一开始棋盘上布满蜘蛛,可以安排每个蜘蛛想邻近的四个方向走,一秒走一次,问一秒以后空格最多有多少。这道题算是这套题最简单的了,很容易想到用dp[i][j][k]来表示当前行为i,状态为j,下一行状态为k的最大空格数。
还有一个问题是判断冲突,如何判断呢?bool f(int x, int y, int z){ //return (((x|y|z|(y<<1)|(y>>1))&((1<<m)-1)) == ((1<<m)-1)); int s = x|y|z|(y<<1)|(y>>1); int t = (1<<m) - 1; if((s & t) == t) return true; else return false;}
能理解吗?其实就是把当前状态退会到移动之前的状态一的二进制定是m个1对吧,就是这个原理。
状态转移部分如下:
memset(num, 0, sizeof(num)); // memset(dp, 0, sizeof(dp)); int sta = 1<<m; REPZ(i, n){ REP(j, sta){ REP(k, sta){ dp[i][j][k] = -inf; } } } REP(i, sta){ dp[0][0][i] = 0; num[i] = m - __builtin_popcount(i); } REP(i, n){//枚举行 REP(j, sta){ //当前行状态 REP(k, sta){//下一行状态 REP(l, sta){//下下行状态 if(f(j, k, l)){ dp[i+1][k][l] = max(dp[i+1][k][l], dp[i][j][k] + num[k]); } } } } } int ans = 0; REP(i, sta){ ans = max(ans, dp[n][i][0]); } printf("%d\n", ans);
【D】倒是觉得这题和这个专题没多大关系,仅仅就是个或运算而已。说下题意吧,就是现在有一颗树有两个操作,把节点i的子树染成某一种颜色c,还有一种是查询一个点的子树有多少种颜色? 就是把区间染色问题搬到了树上而已,我们只需要处理这颗树每个节点的DFS序就可以转化成区间染色问题了,这里颜色有60种,可以LL表示,我依靠自己习惯,随手压了个bitset,不过代码会稍微长一点,不过花点时间就可以了,没有难度。
代码如下:
int n, q, col[maxn];vector <int> G[maxn];//处理DFS序int st[maxn], en[maxn], dfn;void dfs(int u, int fa){ st[u] = ++dfn; for(int i = 0; i < (int)G[u].size(); i++){ int v = G[u][i]; if(v == fa) continue; dfs(v, u); } en[u] = dfn;}//定义线段树节点struct node{ int l, r, lazy; bitset <61> c; node(){} node(int l, int r):l(l), r(r){c.reset();} void SetColor(int col){ c.reset(); c[col] = 1; }}T[maxn * 4];//线段树部分node operator+(node a, node b){ node ret(a.l, b.r); ret.c = a.c | b.c; return ret;}void pushup(int rt){ T[rt].c = T[rt*2].c | T[rt*2+1].c;}void pushdown(int rt){ if(T[rt].lazy){ T[rt*2].SetColor(T[rt].lazy); T[rt*2+1].SetColor(T[rt].lazy); T[rt*2].lazy = T[rt*2+1].lazy = T[rt].lazy; T[rt].lazy = 0; }}void Build(int l, int r, int rt){ T[rt].l = l, T[rt].r = r, T[rt].lazy = 0; if(l == r){ return ;} int mid = (l + r) / 2; Build(l, mid, rt*2); Build(mid+1, r, rt*2+1);}void update(int L, int R, int c, int rt){ if(L == T[rt].l && R == T[rt].r){ T[rt].lazy = c; T[rt].SetColor(c); return ; } pushdown(rt); int mid = (T[rt].l + T[rt].r) / 2; if(R <= mid) update(L, R, c, rt*2); else if(L > mid) update(L, R, c, rt*2+1); else{ update(L, mid, c, rt*2); update(mid+1, R, c, rt*2+1); } pushup(rt);}node queryans(int L, int R, int rt){ if(L == T[rt].l && R == T[rt].r){ return T[rt]; } pushdown(rt); int mid = (T[rt].l + T[rt].r) / 2; if(R <= mid) return queryans(L, R, rt*2); else if(L > mid) return queryans(L, R, rt*2+1); else{ return queryans(L, mid, rt*2) + queryans(mid + 1, R, rt*2+1); }}int main(){ int n, q; while(scanf("%d%d", &n, &q) != EOF) { for(int i = 1; i <= n; i++) G[i].clear(); for(int i = 1; i <= n; i++) scanf("%d", &col[i]); Build(1, n, 1); for(int i = 1; i < n; i++){ int u, v; scanf("%d%d", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfn = 0; dfs(1, -1); for(int i = 1; i <= n; i++){ update(st[i], st[i], col[i], 1); } while(q--) { int op, x, y; scanf("%d", &op); if(op == 1) { scanf("%d%d", &x, &y); update(st[x], en[x], y, 1); } else { scanf("%d", &x); node ans = queryans(st[x], en[x], 1); int sum = ans.c.count(); printf("%d\n", sum); } } } return 0;}
【E】状压+二分好题。大佬昨天讲得很清楚了。把他的方法再复述一次,首先题意是:给你一个长度为N的一个序列,需要我们找到一个最长的序列,保证每个元素(1~8)的个数之前的绝对值差小于等于1,而且对应选出来的序列中,需要保证相同的元素是连续的。解题方法:copy from:点击打开链接 感谢。
1、首先我们很容易想到状压,我们肯定是状压8个数字的状态,其中1表示这个位子上的数已经选完了,0表示没有选完,那么我们在想dp数组的维度的时候,以及初算时间复杂度的时候,发现这个出现的次数一直是一个很难处理的点,那么我们首先对这个出现的次数进行优化:
①我们可以通过枚举来dp,每次枚举一个出现的次数,进行最大值的维护 ,我们不难想到:对应这个出现的次数越多,结果就可能越长,那么其具有单调性。
②对于可以枚举的具有单调性存在的变量,我们可以通过二分查找来进行优化。
2、那么我们每一次二分枚举出的当前值mid,表示本次二分枚举出来每个数将要出现的次数。
①对于当前值mid,每种数字出现的次数要么是mid次,要么是mid-1次。
②考虑dp,我们设定dp【i】【j】表示dp过程进行到第i位,对应j状态下的最多出现mid次的数字的个数。
③那么考虑状态转移方程:
如果我们直接暴力的话肯定时间复杂度是吃不消的,那么我们考虑调用一个vector<int >mp;mp【i】用于存储数字i出现的各个位子,我们按照升序排列存储。
然后状态转移见下面:④维护好每次二分情况的最大值即可。
代码如下:
int n, ans, a[maxn], cur[10], dp[maxn][1<<8];//状态里面0的个数代表长度为len+1的个数vector <int> v[maxn];bool check(int len){ int mask = 1<<8; for(int i = 1; i <= 8; i++) cur[i] = 0; memset(dp, -1, sizeof(dp)); dp[1][0] = 0; for(int i = 1; i <= n; i++){ for(int j = 0; j < mask; j++){ if(dp[i][j] < 0) continue; for(int k = 1; k <= 8; k++){ if(j & 1<<(k-1)) continue; if(cur[k] + len - 1 > (int)v[k].size()) continue; dp[v[k][cur[k] + len - 2]][j | (1<<(k-1))] = max(dp[v[k][cur[k] + len - 2]][j | (1<<(k-1))], dp[i][j]); if(cur[k] + len > (int)v[k].size()) continue; dp[v[k][cur[k] + len - 1]][j | (1<<(k-1))] = max(dp[v[k][cur[k] + len - 1]][j | (1<<(k-1))], dp[i][j] + 1); } } cur[a[i]]++; } int sum = -1; for(int i = 1; i <= n; i++){ sum = max(sum, dp[i][(1<<8) - 1]); } if(sum <= 0) return false; ans = max(ans, sum * len + (8 - sum) * (len - 1)); return true;}int main(){ while(scanf("%d", &n) != EOF) { for(int i = 1; i <= 8; i++) v[i].clear(); for(int i = 1; i <= n; i++){ scanf("%d", &a[i]); v[a[i]].push_back(i); } int l = 2, r = n; ans = 0; while(l <= r){ int mid = (l + r) / 2; if(check(mid) > 0) { l = mid + 1; } else{ r = mid - 1; } } if(ans == 0){ for(int i = 1; i <= 8; i++) if(v[i].size() > 0) ans++; } printf("%d\n", ans); } return 0;}
【F】经典开关灯问题。题意就是有一个n*m的矩形,里面有n*m颗灯,现在问能否找到一个最佳方案使得所有的灯都被关掉,注意一个灯被关掉,则他上下左右的灯都亮了,如果这个题不会做,只能说你这的POWER OJ真的白刷了,因为上面有一个和这个一样的题目。那么我来考虑一下这个题怎么做吧,这题最重要的是要发现一个规律,就是如果你确定了第一行的状态,接下来m-1行的状态都可以递推出来。然后这个题还要求字典序最小的方案,状态直接从小到大枚举,本身就保证了这个特性,所以这个说和不说也差不多吧。
代码如下:
int n, m, step;int maze[16][16], b[16][16], ans[16][16];int dir[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};bool check(int x, int y){ if(x > -1 && y > - 1) return true; else return false;}void press(int i, int j){ ++step; ans[i][j] = 1; b[i][j] = !b[i][j]; REP(k, 4){ int dx = i + dir[k][0]; int dy = j + dir[k][1]; if(check(dx, dy)){ b[dx][dy] ^= 1; } }}bool f(int sta){ step = 0; REP(i, n){ REP(j, m){ b[i][j] = maze[i][j]; } } REP(i, m){ if(sta & (1<<i)){ press(0, i); } } REPZ(i, n){ REP(j, m){ if(b[i-1][j]){ press(i, j); } } } REP(i, m){ if(b[n-1][i]){ return false; } } return true;}int main(){ while(scanf("%d%d", &n,&m) != EOF) { REP(i, n){ REP(j, m){ scanf("%d", &maze[i][j]); } } int sum = 0x3f3f3f3f, nexcur = -1; REP(i, 1<<m){ if(f(i) && step < sum){ sum = step; nexcur = i; } } memset(ans, 0, sizeof(ans)); if(nexcur == -1){ printf("IMPOSSIBLE\n"); } else{ f(nexcur); REP(i, n){ REP(j, m){ if(j == m - 1) printf("%d\n", ans[i][j]); else printf("%d ", ans[i][j]); } } } } return 0;}
【G】已经写过了,就不写了,顺便说一下,这个题也可以用3进制压缩,方法和B的方法完全一样,代码长度可能稍微长一些,贴一个之间的解题链接:点击打开链接
【后记】写到这里,这个专题就写完了,对于刚学的确有一定难度,不过死活把他啃下去之后,你会发现其实状压也不过如此。然后还有什么问题,和不懂的欢迎交流。我自己还有一套状压的训练计划,有余力的可以留言。
- SWUST状态压缩入门 解题报告
- 状态压缩DP POJ 1699解题报告
- poj 2441Arrange the Bulls解题报告-状态压缩dp
- poj 2288 Islands and Bridges解题报告-状态压缩dp
- 解题报告:POJ_2923 Relocation 状态压缩+01背包
- POJ 1753 Flip Game (状态压缩+BFS) -- 解题报告
- 炮兵阵地(状态压缩dp)解题报告
- [hihoCoder] 压缩字符串 解题报告
- Codeforces Round #235 (Div. 2) D. Roman and Numbers 解题报告(状态压缩)
- CF 453B Little Pony and Harmony Chest 解题报告(状态压缩DP)
- hdu 1044 Collect More Jewels 解题报告 + 状态压缩分析
- hdu 4774 Stealing Harry Potter's Precious 状态压缩+bfs 解题报告
- 状态压缩DP 入门
- 状态压缩DP入门
- 状态压缩dp入门
- 状态压缩DP入门
- 状态压缩DP入门
- 状态压缩入门 POJ
- 测绘坐标系统与坐标转换
- C++:STL vector讲解
- 用 PullToRefreshLayout 实现 RecyclerView 的下拉刷新
- 哈弗曼编/译码 项目(可解压缩文本,可执行文件,png,jpg类图片)
- HTML5 + AJAX ( jQuery版本 ) 文件上传带进度条
- SWUST状态压缩入门 解题报告
- 2016年12月22日学习总结----结构体
- MongoDB索引管理-索引的创建、查看、删除
- 抽象工厂模式-与-工厂方法模式区别
- Commit message 和 Change log 编写指南
- MongoDB随笔3:使用索引
- 重构-改善既有代码的设计
- ds作业(优先队列&贪心)
- linux Ubuntu中如何连接xshell