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的方法完全一样,代码长度可能稍微长一些,贴一个之间的解题链接:点击打开链接


【后记】写到这里,这个专题就写完了,对于刚学的确有一定难度,不过死活把他啃下去之后,你会发现其实状压也不过如此。然后还有什么问题,和不懂的欢迎交流。我自己还有一套状压的训练计划,有余力的可以留言。


0 0