CCF-CSP-201409-5 拼图


(本篇旨在让大家能看懂代码,优化之类的SAO操作都没有~~)

一、题目

问题描述
  给出一个n×m的方格图,现在要用如下L型的积木拼到这个图中,使得方格图正好被拼满,请问总共有多少种拼法。其中,方格图的每一个方格正好能放积木中的一块。积木可以任意旋转。
  积木
输入格式
  输入的第一行包含两个整数n, m,表示方格图的大小。
输出格式
  输出一行,表示可以放的方案数,由于方案数可能很多,所以请输出方案数除以1,000,000,007的余数。
样例输入
6 2
样例输出
4
样例说明
  四种拼法如下图所示:
拼法
评测用例规模与约定
  在评测时将使用10个评测用例对你的程序进行评测。
  评测用例1和2满足:1<=n<=30,m=2。
  评测用例3和4满足:1<=n, m<=6。
  评测用例5满足:1<=n<=100,1<=m<=6。
  评测用例6和7满足:1<=n<=1000,1<=m<=6。
  评测用例8、9和10满足:1<=n<=10^15,1<=m<=7。

二、详细讲解

本人初见此题一点思路也没有,思考半小时推导公式等无果,遂放弃,求救度娘,看到了满分结果大都是状态压缩动态规划(状压dp),再加上矩阵快速幂进行求解,但由于大佬们状压dp代码对于我这样不常用C/C++语言的人来说,位运算有些烧脑,难以阅读。最终,我在进一步从leetcode上学习某篇文章后,才理解了大佬们的代码,受益良多。

1.思考(本体难点!)

以m为4时为例。

在这里插入图片描述

(哇手画的好丑,有没有画这种图的软件推荐(=_=)!!)
可以观察发现,下一行的状态仅由这一行的状态决定
假如将每一行看作二进制数字(例如0000为状态0,1010为状态10,1001为状态9等等),那么:
本行状态为0,(填满本行后)下一行分别可能为状态10,9,6,5;
本行状态若为10,下一行状态只可能为15;
本行状态为9,下一行状态可能为15,4,2;

注:每次需填满本行,填不满直接抛弃
那么由于m=4,总共可能有2的四次方也就是16种状态,那么便可以做出一个16*16的状态转移矩阵(在我的代码中命名为A[128][128],因为题目要求m不大于7,状态数不超过128),这个矩阵的i行j列为1,表示如果nowline为状态j,那么nextline的状态可能是i(也有可能是j列的其他为1的行数代表的状态)。

下图为m=4时的状态转移矩阵,可能有人注意到有些位置是2,比如7行8列,其含义实际上是从状态8转移到状态7有两种方式(可以自己在纸上画一下)。
也可以用来debug(0-15是行号列号,纯手打)
在这里插入图片描述
那么,我们求这个矩阵的意义是什么?

用矩阵乘以(1,0,…,0)T(不会打转置,其实就是竖着的(1,0,…,0))会得到什么呢?
会得到第一竖列(状态向量),也就是状态0能转到哪些状态;
如果用得到的一竖列(状态向量)再乘一次这个矩阵,就会得到乘之前的状态能转到哪些状态,而有些数字将不再是1,这代表着有不同的方式都能转到此状态,能转到此状态的方式数正是状态向量对应位置乘出来的数字。
而每次状态转移,相当于添加了一行(1,1,1,1),也就意味着,我们需要从状态0开始,进行n-1次状态转移,最终转移到(1,1,1,1),就可以填满n*m矩阵。
这也就意味着,n-1次状态转移后,状态向量中对应状态是15的位置上的数字,就代表着从第一行是(0,0,0,0)开始,到第n行状态是(1,1,1,1),有多少种方式可以转移,即为所求。

PS:也可以求n次状态转移,然后看状态0的数量,我的代码是这样求的(懒得减一了Orz)。

弄懂上边这些,其实这个题的难度将会骤降,这些才是本题难点,估计也是令人望而生畏的地方所在。
想完全搞懂此题必须弄懂上边这部分,而且这部分感觉偏数学。

2.动态规划

写到这里其实已经没有很难的地方了。
也就是用动态规划方法求矩阵A。
我采用的是递归方法。
复制粘贴谁也会,那么这个…思想很重要,很多大佬采用了位压缩,是一个不错的思想,但可能阅读存在困难?总之我阅读存在困难,可能我不经常用吧。

解释下我的代码:
首先是一个函数,将数组看作二进制数,例如{1,0,1,0}看作10并输出;
然后是初始化了A矩阵;
然后是dfs递归求矩阵A,其中i代表正在试图在这一行的第几个位置放置拼图,m是输入的m,nowline代表当前这一行,nextline表示下一行。
如果i=m,表示这一行完整拼完了,那么改变矩阵A;
如果nowLine[i]为1,表示这一个位置已经拼过了,那么拼下一个位置;
下边是四种拼图放置方法对应的位子有空位,就拼上此位置继续拼,记得递归完成后“拆掉”拼过的再去考虑其他的。
然后在下边的main函数可以看到调用dfs时,对每一种nowLine的情况都进行了遍历,遍历nowLine使用了arrPlus函数,此函数将输入的nowLine看作二进制数并加一后返回,即输入{1,0,1,0},返回{1,0,1,1}。

int arrToInt(int arr[], int L) {
    //将数组看作二进制数,输出此数
    int i = 0;
    for (int j = 0; j < L; j++) {
        i *= 2;
        if (arr[j] == 1) {
            i += 1;
        }
    }
    return i;
}

int A[128][128] = {0};

void dfs(int i, int m, int nowLine[], int nextLine[]) {
    if (i == m) {
        A[arrToInt(nextLine, m)][arrToInt(nowLine, m)]++;
        return;
    }
    if (nowLine[i] == 1) {
        dfs(i + 1, m, nowLine, nextLine);
    }
    if (i >= 0 && i + 1 < m && nowLine[i] == 0 && nowLine[i + 1] == 0 && nextLine[i] == 0) {
        nextLine[i] = 1;
        dfs(i + 2, m, nowLine, nextLine);
        nextLine[i] = 0;
    }
    if (i >= 0 && i + 1 < m && nowLine[i] == 0 && nowLine[i + 1] == 0 && nextLine[i + 1] == 0) {
        nextLine[i + 1] = 1;
        dfs(i + 2, m, nowLine, nextLine);
        nextLine[i + 1] = 0;
    }
    if (i >= 0 && i + 1 < m && nowLine[i] == 0 && nextLine[i] == 0 && nextLine[i + 1] == 0) {
        nextLine[i] = 1;
        nextLine[i + 1] = 1;
        dfs(i + 1, m, nowLine, nextLine);
        nextLine[i] = 0;
        nextLine[i + 1] = 0;
    }
    if (i > 0 && i < m && nowLine[i] == 0 && nextLine[i] == 0 && nextLine[i - 1] == 0) {
        nextLine[i] = 1;
        nextLine[i - 1] = 1;
        dfs(i + 1, m, nowLine, nextLine);
        nextLine[i] = 0;
        nextLine[i - 1] = 0;
    }
}

void arrPlus(int nowLine[], int m) {
    //将数组看作二进制数,数组代表的的二进制数增加1
    for (int i = m - 1; i >= 0; i--) {
        if (nowLine[i] == 0) {
            nowLine[i]++;
            break;
        } else {
            nowLine[i] = 0;
        }
    }
}
int main() {
    long long n;
    int m;
    cin >> n >> m;
    int nowLine[7] = {0};
    int nextLine[7] = {0};
    for (int i = 0; i < (1 << m); i++) {
        dfs(0, m, nowLine, nextLine);
        arrPlus(nowLine, m);
    }
    rec_mult(m, n);
    cout << ans[0][0];
    return 0;
}

在看完上面的内容后,如果自己进行尝试矩阵部分的代码尝试,其实也是可以写出正确的程序,但是会超时,因为n实在是太大了。
如果得了70,那应该就代表矩阵没求错。
在这里插入图片描述
附一些较小的n对应的测试数据,加了个回车方便复制:(来自岩之痕)
https://blog.csdn.net/zearot/article/details/44680459
输入:2 3

输出:2

输入:3 7

输出:0

输入:4 6

输出:18

输入:9 5

输出:384

输入:30 2

输出:1024

输入:1 1

输出:0

输入:9 2

输出:8

输入:924 6

输出:584569618

3.矩阵快速幂

这部分十分简单,却是优化的关键,直接把n带来的o(n)时间复杂度优化到了o(lgn),而本题中n非常大,优化十分明显。
我们从前文已经知道,我们要计算A与一个向量相乘后,再用A相乘,这样的操作重复n次,从矩阵乘法的一些知识,我们知道,这其实与计算A的n次方,再与向量相乘是等价的,而A的n次方并没有必要从1一直乘上去,这样乘n次必超时,可以用以下方法:
以n=157=10011101(二进制)为例:
从单位矩阵开始;
由于二进制最后一位为1,先乘一个A;
二进制倒数第二位为0,计算A的平方,但不乘;
二进制倒数第三位为1,计算A的平方乘以A的平方,并相乘;
倒数第四位为1,计算刚刚得到的A的四次方乘以A的四次方,并相乘;
以此类推,便能乘o(lgn)次得到A的n次方。
具体实现如下:

void rec_self_mult(int m) {
    //会把A*A储存进A中
    int i, s, t;
    for (s = 0; s < m; s++) {
        for (t = 0; t < m; t++) {
            int sum = 0;
            for (i = 0; i < m; i++) {
                sum = (int) (sum + (ll * A[s][i] * A[i][t]) % 1000000007) % 1000000007;
            }
            temp[s][t] = sum;
        }
    }
    for (s = 0; s < m; s++) {
        for (t = 0; t < m; t++) {
            A[s][t] = temp[s][t];
        }
    }
}

void rec_mult(int m0, long long n) {
    //A的n次方,存储在ans中
    int i, j, s, t;
    int m = (1 << m0);
    for (i = 0; i < m; i++) {
        ans[i][i] = 1;
    }
    while (n != 0) {
        if (n % 2 == 1) {
            for (s = 0; s < m; s++) {
                for (t = 0; t < m; t++) {
                    int sum = 0;
                    for (j = 0; j < m; j++) {
                        sum = (int) (sum + (ll * ans[s][j] * A[j][t]) % 1000000007) % 1000000007;
                    }
                    temp[s][t] = sum;
                }
            }
            for (s = 0; s < m; s++) {
                for (t = 0; t < m; t++) {
                    ans[s][t] = temp[s][t];
                }
            }
        }
        rec_self_mult(m);
        n = n / 2;
    }
}

其实很多地方是矩阵相乘,看上去复杂,原理其实很简单。
附测试数据,仍然是刚刚的大佬的,加了个回车方便复制:(来自岩之痕)
https://blog.csdn.net/zearot/article/details/44680459
输入:924 6

输出:584569618

输入:99999999999999 6

输出:917572776

输入:999999999999999 7

输出:847356131

(忙活一下午唉)
在这里插入图片描述

总结(以及AC代码)

大概可能有人想直接抄?那给你啦~

#include <iostream>

using namespace std;

long long ll = 1;

int arrToInt(int arr[], int L) {
    //将数组看作二进制数,输出此数
    int i = 0;
    for (int j = 0; j < L; j++) {
        i *= 2;
        if (arr[j] == 1) {
            i += 1;
        }
    }
    return i;
}

void arrPlus(int nowLine[], int m) {
    //将数组看作二进制数,数组代表的的二进制数增加1
    for (int i = m - 1; i >= 0; i--) {
        if (nowLine[i] == 0) {
            nowLine[i]++;
            break;
        } else {
            nowLine[i] = 0;
        }
    }
}

int A[128][128] = {0};
int temp[128][128] = {0};
int ans[128][128] = {0};

void dfs(int i, int m, int nowLine[], int nextLine[]) {
    if (i == m) {
        A[arrToInt(nextLine, m)][arrToInt(nowLine, m)]++;
        return;
    }
    if (nowLine[i] == 1) {
        dfs(i + 1, m, nowLine, nextLine);
    }
    if (i >= 0 && i + 1 < m && nowLine[i] == 0 && nowLine[i + 1] == 0 && nextLine[i] == 0) {
        nextLine[i] = 1;
        dfs(i + 2, m, nowLine, nextLine);
        nextLine[i] = 0;
    }
    if (i >= 0 && i + 1 < m && nowLine[i] == 0 && nowLine[i + 1] == 0 && nextLine[i + 1] == 0) {
        nextLine[i + 1] = 1;
        dfs(i + 2, m, nowLine, nextLine);
        nextLine[i + 1] = 0;
    }
    if (i >= 0 && i + 1 < m && nowLine[i] == 0 && nextLine[i] == 0 && nextLine[i + 1] == 0) {
        nextLine[i] = 1;
        nextLine[i + 1] = 1;
        dfs(i + 1, m, nowLine, nextLine);
        nextLine[i] = 0;
        nextLine[i + 1] = 0;
    }
    if (i > 0 && i < m && nowLine[i] == 0 && nextLine[i] == 0 && nextLine[i - 1] == 0) {
        nextLine[i] = 1;
        nextLine[i - 1] = 1;
        dfs(i + 1, m, nowLine, nextLine);
        nextLine[i] = 0;
        nextLine[i - 1] = 0;
    }
}

void rec_self_mult(int m) {
    //会把A*A储存进A中
    int i, s, t;
    for (s = 0; s < m; s++) {
        for (t = 0; t < m; t++) {
            int sum = 0;
            for (i = 0; i < m; i++) {
                sum = (int) (sum + (ll * A[s][i] * A[i][t]) % 1000000007) % 1000000007;
            }
            temp[s][t] = sum;
        }
    }
    for (s = 0; s < m; s++) {
        for (t = 0; t < m; t++) {
            A[s][t] = temp[s][t];
        }
    }
}

void rec_mult(int m0, long long n) {
    //A的n次方,存储在ans中
    int i, j, s, t;
    int m = (1 << m0);
    for (i = 0; i < m; i++) {
        ans[i][i] = 1;
    }
    while (n != 0) {
        if (n % 2 == 1) {
            for (s = 0; s < m; s++) {
                for (t = 0; t < m; t++) {
                    int sum = 0;
                    for (j = 0; j < m; j++) {
                        sum = (int) (sum + (ll * ans[s][j] * A[j][t]) % 1000000007) % 1000000007;
                    }
                    temp[s][t] = sum;
                }
            }
            for (s = 0; s < m; s++) {
                for (t = 0; t < m; t++) {
                    ans[s][t] = temp[s][t];
                }
            }
        }
        rec_self_mult(m);
        n = n / 2;
    }
}

int main() {
    long long n;
    int m;
    cin >> n >> m;
    int nowLine[7] = {0};
    int nextLine[7] = {0};
    for (int i = 0; i < (1 << m); i++) {
        dfs(0, m, nowLine, nextLine);
        arrPlus(nowLine, m);
    }
    rec_mult(m, n);
    cout << ans[0][0];
    return 0;
}

注意:n超出int范围了,用long long。
我只是个搬运工,算法都是学习的大佬的,只不过没有使用位压,可能初学者更易看懂,不过我还是提一句吧:
位压dp其实就是把nowline代表的二进制数直接用int而不是数组进行表示,在修改时,采用位运算代替我们的数组操作,这样的好处是:代码量会减少,内存空间的占用会减少,但实际上思想是一致的。
我是准备考ccf-csp,4月11日考,考过了的话估计很长时间不会碰c/c++,没过的话,应该还会写很多…快祝我断更。
参考:
https://blog.csdn.net/qq_45228537/article/details/102804805
https://blog.csdn.net/u011077606/article/details/43487421
https://blog.csdn.net/wust_zzwh/article/details/52058209
https://blog.csdn.net/zearot/article/details/44680459
https://leetcode-cn.com/problems/broken-board-dominoes/solution/c-zhuang-tai-ya-suo-dong-tai-gui-hua-by-newhar/
另外,有兴趣可以看http://poj.org/problem?id=2411,相信对于学完本篇文章的你来说很简单了。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值