《挑战程序设计竞赛》3.4.2 动态规划-矩阵幂 POJ3070 3734 3233 2663 3420 3735


POJ3070

http://poj.org/problem?id=3070

题意

求斐波那契数列第n项的值对10^4取余的结果。

思路

由于n很大,常见的数组递推肯定是不行的。要用矩阵幂来做,时间复杂度O(logN)。这里不再讲解,书中以及题目中都有讲解或提示。

代码

Source Code

Problem: 3070       User: liangrx06
Memory: 176K        Time: 32MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

typedef vector<int> vec;
typedef vector<vec> mat;

const int M = 10000;
const int S = 2;

mat mult(mat &a, mat &b)
{
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++) {
        for (int j = 0; j < S; j ++) {
            for (int k = 0; k < S; k ++) {
                c[i][j] = (c[i][j] + a[i][k]*b[k][j]) % M;
            }
        }
    }
    return c;
}

mat pow(mat &a, int k)
{
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++)
        c[i][i] = 1;
    while (k) {
        if (k & 1) c = mult(c, a);
        k >>= 1;
        a = mult(a, a);
    }
    return c;
}

int main(void)
{
    int n;
    mat a(S, vec(S, 0)), c(S, vec(S, 0));

    while (scanf("%d", &n) != EOF) {
        if (n == -1) break;

        a[0][0] = a[0][1] = a[1][0] = 1;
        a[1][1] = 0;
        c = pow(a, n);
        printf("%d\n", c[0][1]);
    }

    return 0;
}

POJ3734

http://poj.org/problem?id=3734

题意

给N个方块排成一列。现在要用红、蓝、绿、黄四种颜色的油漆给这些方块染色。求染成红色方块和染成绿色方块的个数同时为偶数的染色方案的个数,输出对10007取余后的答案。(1<=n<=10^9)。

思路

这是书中例题,书中给出的解法是:

 当我们准备准备染第i个方块的时候,前i-1个方块已经染好颜色了。对于我们的目标颜色红色和绿色,根据我们想要红绿同时为偶的想法,我们可以把前i-1个方块的染色分成3种情况:

    1.红色和绿色同时为偶数的方案数

    2.红色和绿色中一个偶数一个奇数的方案数

    3.红色和绿色同时为奇数的方案数

我们记前3种情况的方案数为ai-1,bi-1,ci-1。

我们最后需要的是an,但是我们需要a,b,c进行递推。

根据以上想法,我们希望用ai-1,bi-1,ci-1推出ai,bi,ci。

那么,多一个格子染色方案有多少个,是很容易推出来的哦:

    ai=2*ai-1+bi-1;

    bi=2*ai-1+2*bi-1+2*ci-1;

    ci=bi-1+2*c-1;

根据这个递推式,我们可以得到矩阵A:

2   1   0
2   2   2
0   1   2


矩阵B为i=0的时候a,b,c的值:

1
0
0

然后用同样的快速幂求矩阵乘法的方法求解。

另外还可以将情况分成4种导出递推关系式然后矩阵幂。
更甚至还可以用组合数学的知识进行分析求解。
这两种方法的介绍见博客:[POJ 3734] Blocks (矩阵快速幂、组合数学)

代码

Source Code

Problem: 3734       User: liangrx06
Memory: 244K        Time: 47MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

typedef vector<int> vec;
typedef vector<vec> mat;

const int M = 10007;
const int S = 3;

mat mult(mat &a, mat &b)
{
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++) {
        for (int j = 0; j < S; j ++) {
            for (int k = 0; k < S; k ++) {
                c[i][j] = (c[i][j] + a[i][k]*b[k][j]) % M;
            }
        }
    }
    return c;
}

mat pow(mat &a, int k)
{
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++)
        c[i][i] = 1;
    while (k) {
        if (k & 1) c = mult(c, a);
        k >>= 1;
        a = mult(a, a);
    }
    return c;
}

int main(void)
{
    int t, n;
    mat a(S, vec(S, 0));

    cin >> t;
    while (t--) {
        scanf("%d", &n);
        a[0][0] = 2, a[0][1] = 1, a[0][2] = 0;
        a[1][0] = 2, a[1][1] = 2, a[1][2] = 2;
        a[2][0] = 0, a[2][1] = 1, a[2][2] = 2;
        a = pow(a, n);
        printf("%d\n", a[0][0]);
    }

    return 0;
}

POJ3233

http://poj.org/problem?id=3233

题意

给你A矩阵,A矩阵是n*n的一个矩阵,现在要你求S = A + A^2 + A^3 + … + A^k.
那么s一定也是一个N*N的矩阵,最后要你输出s,并且s的每一个元素对m取余数。

思路

求a^1+..a^n这是矩阵乘法中关于等比矩阵的求法:

|A E|

|0 E|

其中的A为m阶矩阵,E是单位矩阵,0是零矩阵。而我们要求的是:

A^1+A^2+..A^L,由等比矩阵的性质

|A , 1| |A^n , 1+A^1+A^2+….+A^(n-1)|

|0 , 1| 的n次方等于|0 , 1 |

所以我们只需要将A矩阵扩大四倍,变成如上形式的矩阵B,然后开L+1次方就可以得到1+A^1+A^2+….+A^L。由于多了一个1,所以最后得到的答案我们还要减去1。同理我们把矩阵A变成B:

        |A  E|

        |0  E|

然后我们就是求B的n+1次幂之后得到的矩阵为|x1 x2|

        |x3   x4|

右上角的矩阵x2减去单位矩阵E,得到就是要求的矩阵了!

代码

Source Code

Problem: 3233       User: liangrx06
Memory: 292K        Time: 704MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

typedef vector<int> vec;
typedef vector<vec> mat;

int n, k, m;

mat mult(mat &a, mat &b)
{
    int S = a.size();
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++) {
        for (int j = 0; j < S; j ++) {
            for (int k = 0; k < S; k ++) {
                c[i][j] = (c[i][j] + a[i][k]*b[k][j]) % m;
            }
        }
    }
    return c;
}

mat pow(mat &a, int k)
{
    int S = a.size();
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++)
        c[i][i] = 1;
    while (k) {
        if (k & 1) c = mult(c, a);
        k >>= 1;
        a = mult(a, a);
    }
    return c;
}

int main(void)
{
    cin >> n >> k >> m;
    mat a(n, vec(n, 0));
    mat c(2*n, vec(2*n, 0));
    for (int i = 0; i < n; i ++) {
        for (int j = 0; j < n; j ++) {
            scanf("%d", &a[i][j]);
            c[i][j] = a[i][j];
        }
    }
    for (int i = 0; i < n; i ++)
        c[i+n][i+n] = c[i+n][i] = 1;

    c = pow(c, k);
    mat b(n, vec(n, 0));
    for (int i = 0; i < n; i ++)
        for (int j = 0; j < n; j ++)
            b[i][j] = c[i+n][j];
    b = mult(a, b);
    for (int i = 0; i < n; i ++)
        for (int j = 0; j < n; j ++)
            printf("%d%c", b[i][j], (j+1 == n) ? '\n' : ' ');

    return 0;
}

POJ2663

http://poj.org/problem?id=2663

题意

用2 × 1 的方块填充3 × N 的矩形,有多少种方法。

思路

该题是POJ3420的简单版。重点是建立递推关系。然后如果N小就直接递推,N大就要用矩阵幂来做。
我的做法是将状态分为两种情况,然后建立的递推关系是:
a[i] = 3*a[i-1] + b[i-1];
b[i] = 2*a[i-1] + b[i-1];
图实在不好画,这里先偷下懒了。
另外可参考有位仁兄的分析:POJ2663递推关系分析

代码

Source Code

Problem: 2663       User: liangrx06
Memory: 164K        Time: 0MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

int main(void)
{
    int n;
    int a[16], b[16];

    a[0] = 1, b[0] = 0;
    for (int i = 1; i < 16; i++) {
        a[i] = 3*a[i-1] + b[i-1];
        b[i] = 2*a[i-1] + b[i-1];
    }

    while (scanf("%d", &n) != EOF) {
        if (n == -1) break;
        printf("%d\n", (n&1) ? 0: a[n/2]);
    }

    return 0;
}

POJ3420

http://poj.org/problem?id=3420

题意

用2 × 1 的方块填充4 × N (1 ≤ N ≤ 10^9) 的矩形,有多少种方法。结果对M取模。

思路

显然这个题的基本思路是递推,但宽度为4确实又比宽度3麻烦了很多,以至于我始终找不到正确的递推关系式。

后来参考了这篇博客才清楚了:poj 3420 Quad Tiling 矩阵乘法+快速求幂

我们把4当作列数,n当作行数。当第n行填满时,第(n+1)行会出现以下几种情况:
这里写图片描述
情况a为第n行刚好填满且没有突出到第(n+1)行,即为所求答案,由图不难推出:
a[n] = a[n-1] + b[n-1] + c[n-1] + dx[n-1] + dy[n-1];
b[n] = a[n-1];
c[n] = a[n-1] + e[n-1];
dx[n]= a[n-1] + dy[n-1];
dy[n]= a[n-1] + dx[n-1];
e[n] = c[n-1].
令d[n] = dx[n] + dy[n],则
a[n] = a[n-1] + b[n-1] + c[n-1] + d[n-1];
b[n] = a[n-1];
c[n] = a[n-1] + e[n-1];
d[n] = a[n-1] * 2 + d[n-1];
e[n] = c[n-1].
令A[n] = { a[n] , b[n] , c[n] , d[n] , e[n] },则有:
A[n] = A[n-1] X F
其中F等于:
/ 1 1 1 2 0 \
| 1 0 0 0 0 |
| 1 0 0 0 1 |
| 1 0 0 1 0 |
\ 0 0 1 0 0 /
那么A[x] = A[y] X F^(x-y),其中 x >= y,即有 A[n] = A[0] X F^n 。
由每种状态不难看出 A[0] = { 1 , 0 , 0 , 0 , 0 }。
那么就可以用矩阵乘法+快速求幂先算出 F^n,则 F^n 的第一列的和与A[0]的乘积即为答案。其实就是A[0][0]。

代码

Source Code

Problem: 3420       User: liangrx06
Memory: 244K        Time: 16MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

typedef vector<int> vec;
typedef vector<vec> mat;

int n, m;

mat mult(mat &a, mat &b)
{
    int S = a.size();
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++) {
        for (int j = 0; j < S; j ++) {
            for (int k = 0; k < S; k ++) {
                c[i][j] = (c[i][j] + a[i][k]*b[k][j]) % m;
            }
        }
    }
    return c;
}

mat pow(mat &a, int k)
{
    int S = a.size();
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++)
        c[i][i] = 1;
    while (k) {
        if (k & 1) c = mult(c, a);
        k >>= 1;
        a = mult(a, a);
    }
    return c;
}

int main(void)
{
    while (cin >> n >> m, n || m) {
        mat a(5, vec(5, 0));
        a[0][0] = a[0][1] = a[0][2] = 1, a[0][3] = 2;
        a[1][0] = 1;
        a[2][0] = a[2][4] = 1;
        a[3][0] = a[3][3] = 1;
        a[4][2] = 1;

        a = pow(a, n);
        printf("%d\n", a[0][0]);
    }

    return 0;
}

POJ3735

http://poj.org/problem?id=3735

题意

有n只猫咪,开始时每只猫咪有花生0颗,现有一组操作,共k个,由下面三个操作的其中一个组成:
1. g i 给i只猫咪一颗花生米
2. e i 让第i只猫咪吃掉它拥有的所有花生米
3. s i j 将猫咪i与猫咪j的拥有的花生米交换
现将上述一组操作重复做m次后,问每只猫咪有多少颗花生?

思路

m达到10^9,显然不能直接算。
因为k个操作给出之后就是固定的,所以想到用矩阵,矩阵快速幂可以把时间复杂度降到O(logm)。问题转化为如何构造转置矩阵?
说下我的思路,观察以上三种操作,发现第二,三种操作比较容易处理,重点落在第一种操作上。
有一个很好的办法就是添加一个辅助,使初始矩阵变为一个n+1元组,编号为0到n,下面以3个猫为例:
定义初始矩阵A = [1 0 0 0],0号元素固定为1,1~n分别为对应的猫所拥有的花生数。
对于第一种操作g i,我们在单位矩阵基础上使Mat[0][i]变为1,例如g 1:
1 1 0 0
0 1 0 0
0 0 1 0
0 0 0 1,显然[1 0 0 0]*Mat = [1 1 0 0]
对于第二种操作e i,我们在单位矩阵基础使Mat[i][i] = 0,例如e 2:
1 0 0 0
0 1 0 0
0 0 0 0
0 0 0 1, 显然[1 2 3 4]*Mat = [1 2 0 4]
对于第三种操作s i j,我们在单位矩阵基础上使第i列与第j互换,例如s 1 2:
1 0 0 0
0 0 0 1
0 0 1 0
0 1 0 0,显然[1 2 0 4]*Mat = [1 4 0 2]
现在,对于每一个操作我们都可以得到一个转置矩阵,把k个操作的矩阵相乘我们可以得到一个新的转置矩阵T。
A * T 表示我们经过一组操作,类似我们可以得到经过m组操作的矩阵为 A * T ^ m,最终矩阵的[0][1~n]即为答案。
上述的做法比较直观,但是实现过于麻烦,因为要构造k个不同矩阵。
有没有别的方法可以直接构造转置矩阵T?答案是肯定的。
我们还是以单位矩阵为基础:
对于第一种操作g i,我们使Mat[0][i] = Mat[0][i] + 1;
对于第二种操作e i,我们使矩阵的第i列清零;
对于第三种操作s i j,我们使第i列与第j列互换。
这样实现的话,我们始终在处理一个矩阵,免去构造k个矩阵的麻烦。
至此,构造转置矩阵T就完成了,接下来只需用矩阵快速幂求出 A * T ^ m即可,还有一个注意的地方,该题需要用到long long。

我最开始没有任何优化时,超时TLE;
免去构造k个矩阵,而始终只处理一个矩阵后,仍然超时;
最后在矩阵乘的函数中加上了对稀疏矩阵的特殊处理,终于AC,时间是1795ms。
但其他人加入了这些优化的代码能达到100ms,我仔细找了一下原因,认为整体思路基本上是一样的,可能是vector操作相比一般数组操作要更耗时

代码

Source Code

Problem: 3735       User: liangrx06
Memory: 508K        Time: 1750MS
Language: C++       Result: Accepted
Source Code
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

typedef long long LL;
typedef vector<LL> vec;
typedef vector<vec> mat;

mat mult(mat &a, mat &b)
{
    int S = a.size();
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++) {
        for (int j = 0; j < S; j ++) {
            if (a[j][i]) {
                for (int k = 0; k < S; k ++) {
                    c[j][k] += a[j][i]*b[i][k];
                }
            }
        }
    }
    return c;
}

mat pow(mat &a, int k)
{
    int S = a.size();
    mat c(S, vec(S, 0));
    for (int i = 0; i < S; i ++)
        c[i][i] = 1;
    while (k) {
        if (k & 1) c = mult(c, a);
        k >>= 1;
        a = mult(a, a);
    }
    return c;
}

int main(void)
{
    int n, m, k;
    while (cin >> n >> m >> k) {
        if (!n && !m && !k) break;

        mat a(n+1, vec(n+1, 0));
        for (int i = 0; i < n; i ++)
            a[i][i] = 1;
        a[n][n] = 1;

        while (k--) {
            char s[2];
            int x, y;
            scanf("%s", s);
            scanf("%d", &x);
            x --;

            if (s[0] == 'g') {
                a[n][x] ++;
            }
            else if (s[0] == 'e') {
                for (int i = 0; i <= n; i ++)
                    a[i][x] = 0;
            }
            else {
                scanf("%d", &y);
                y --;
                for (int i = 0; i <= n; i ++)
                    swap(a[i][x], a[i][y]);
            }
        }

        a = pow(a, m);
        for (int i = 0; i < n; i ++)
            printf("%lld%c", a[n][i], (i+1 == n)? '\n' : ' ');
    }

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值