[关闭]
@Dmaxiya 2020-08-24T23:42:04.000000Z 字数 7403 阅读 2916

从快速幂到 dp 优化:矩阵快速幂

博文


幂运算

相加我们当然不会写成一个循环, 相乘我们自然要用幂运算。

幂运算裸题

题目链接

PAT L1-012: 计算指数

题意

输入数字 ,求出 的值,

解法

用上 cmath 头文件中的 pow 函数即可,由于本题数据范围极小,所以根本不会出现什么精度问题。

过题代码

  1. #include <iostream>
  2. #include <cmath>
  3. using namespace std;
  4. int main() {
  5. int n;
  6. cin >> n;
  7. cout << 2 << "^" << n << " = " << pow(2, n) << endl;
  8. return 0;
  9. }

快速幂运算

这只是相对于比较小的数据而言,但是如果要计算的数据范围较大,大到连 double 都存不下时,用 pow 函数显然会输出错误的结果,而如果写一个循环,显然不可能在规定时间内得到结果,如果题目没有要求结果对某个数取模,算一算结果的位数,大概就要用到 Java 的 BigInteger 类,不过以下仅讨论结果对某个数取模时的写法,即使要用到 BigInteger 类,也是相同的写法,只是换个语言罢了。
先在这里提以下两个取模公式


证明略。

算法思想

对于幂运算我们知道,


有了上面两个公式,我们就可以将幂运算进行下面的转换:

而右边小括号里的 又是一个幂运算,于是我们就可以递归转换下去,直到 。现在来看一下这样转换之后的计算次数有多少次:
如果是要计算 ,就要先计算 ,要计算这个值,就要先计算 以这样的方式递减下去,我们可以看到,将在 的次数内下降至 ,所以要计算 的时间复杂度就为

幂运算题改

题意

时求出 的结果,由于该结果非常大,所以只要求输出 的值。

题解:快速幂

代码

  1. #include <iostream>
  2. using namespace std;
  3. #define LL long long
  4. const LL MOD = 1000000007;
  5. LL mi(LL res, LL n) {
  6. LL ans;
  7. for(ans = 1; n != 0; n >>= 1) {
  8. if(n % 2 == 1) {
  9. ans = (ans * res) % MOD;
  10. }
  11. res = (res * res) % MOD;
  12. // cout << "ans = " << res << "^" << n / 2 << " * " << ans << endl << endl;
  13. // cout << "ans = " << ans << " res = " << res << " n = " << n << endl << endl;
  14. }
  15. return ans;
  16. }
  17. int main() {
  18. LL n;
  19. cin >> n;
  20. cout << "2^" << n << " = " << mi(2, n) << endl;
  21. return 0;
  22. }

对代码快速幂函数部分有疑问的,可以去掉循环中两个 cout 语句的注释,来看看每次循环的值的变化是什么样的。

快速幂裸题

题目链接

POJ 1995: Raising Modulo Numbers

题意

给定 的值以及 ,求出


的值,其中 不同时为

题解:快速幂

过题代码

  1. #include <iostream>
  2. using namespace std;
  3. #define LL long long
  4. LL mi(LL res, LL n, LL MOD) {
  5. LL ans;
  6. for(ans = 1; n != 0; n >>= 1) {
  7. if(n % 2 == 1) {
  8. ans = (ans * res) % MOD;
  9. }
  10. res = (res * res) % MOD;
  11. }
  12. return ans;
  13. }
  14. int main() {
  15. ios::sync_with_stdio(false);
  16. LL Z, M, N;
  17. LL ans, a, b;
  18. cin >> Z;
  19. while(Z--) {
  20. ans = 0;
  21. cin >> M >> N;
  22. for(int i = 0; i < N; ++i) {
  23. cin >> a >> b;
  24. ans = (ans + mi(a, b, M)) % M;
  25. }
  26. cout << ans << endl;
  27. }
  28. return 0;
  29. }

简单dp

关于动态规划

dp 是动态规划的简称,动态规划大家第一次听大概是在上课的时候,老师教到递归求斐波那契数列时提到的动态规划记录状态的解法吧。动态规划什么原理什么性的,很多博客已经说得很清楚了,大多是复制粘贴,这里不再赘述。
这里来一个简单的题目:从楼下到楼上有 层台阶,一只青蛙要上楼,它有两种跳法,一种是一步一台阶,一种是一步二台阶,问给定台阶的数量 ,这只青蛙有多少种跳法。
不太熟悉 dp 或者经常做蓝桥杯题目的,可能一看这题就开始写搜索的代码了,确实,很多 dp 也可以说是搜索的优化,因为搜索的状态太多,于是就将重复的搜索状态记录下来,这样就可以减少大量无用的搜索,这大概就是动态规划的作用吧。
从题中我们可以知道,除了第一层和第二层,每到一层台阶,都有两种到达这层台阶的方法,一种是从第 层台阶跳两层上来,一种是从第 层台阶跳一步上来,也就是说,到达第 层台阶的方法数量就是到达第 层的方法数量加上到达第 层的方法数量,设 为到达第 层台阶的跳法,于是有以下递推公式 :


咦,这不就是一个斐波那契吗,然后写个循环就出来了。
个人觉得斐波那契的动态规划解法只是一种循环写法,是个人都能想到,还不能和动态规划扯上什么关系,可能写完了也不知道动态规划是什么,这个例子应该能比较形象地体现出动态规划在算法的优化上的作用。
当然,动态规划可不全是斐波那契数列,这里只是讲个简单的例子。

斐波那契数列

题目链接

九度OJ 1387: 斐波那契数列

题意

输出斐波那契数列第 项,其中

题解

没什么好解的,一个循环预处理,注意 long long 就行了。

过题代码

  1. #include <cstdio>
  2. using namespace std;
  3. #define LL long long
  4. int main() {
  5. int n;
  6. LL fib[100];
  7. fib[0] = 0;
  8. fib[1] = 1;
  9. for(int i = 2; i <= 70; ++i) {
  10. fib[i] = fib[i - 1] + fib[i - 2];
  11. }
  12. while(scanf("%d", &n) != EOF) {
  13. printf("%lld\n", fib[n]);
  14. }
  15. return 0;
  16. }

矩阵快速幂

dp 已经这么快了,还可以优化?看标题 ↑。
看到这里,还记得前面提过的快速幂吗? 个相同的数相乘可以用快速幂, 个相同的矩阵相乘,自然也可以用快速幂。可这和 dp 有什么关系呢?主要是因为:部分 dp 是可以推导出某个能用矩阵相乘的形式表示的递推公式,斐波那契数列就是一个很好的例子。

算法思想

快速幂的背景是大数和取模上面已经说过,不再重复,这里来看如何把斐波那契数列的递推公式表示成矩阵相乘的形式:




对于这种形式的递推公式的矩阵构造应该能够很轻松看得出来吧,其实快速幂的运用不仅仅是在这样的递推公式上,对于某些图将其邻接矩阵表示出来,也可以构造出一个矩阵快速幂,其算法时间复杂度为 ,其中 为构造出的矩阵行列值。

斐波那契矩阵快速幂裸题

题目链接

51Nod 1242: 斐波那契数列的第 N 项

题意

求斐波那契数列第 项值,如果太大则对 取模。

题解:矩阵快速幂

过题代码

  1. #include <cstdio>
  2. #include <cstring>
  3. using namespace std;
  4. #define LL long long
  5. const LL MOD = 1000000009;
  6. const int SIZE = 2;
  7. struct Matrix {
  8. LL num[SIZE][SIZE];
  9. Matrix() {
  10. memset(num, 0, sizeof(num));
  11. for(int i = 0; i < SIZE; ++i) {
  12. num[i][i] = 1;
  13. }
  14. }
  15. void Set() {
  16. num[0][0] = num[0][1] = num[1][0] = 1;
  17. num[1][1] = 0;
  18. }
  19. void operator*=(const Matrix &b) {
  20. LL ans[SIZE][SIZE];
  21. for(int i = 0; i < SIZE; ++i) {
  22. for(int j = 0; j < SIZE; ++j) {
  23. ans[i][j] = 0;
  24. for(int k = 0; k < SIZE; ++k) {
  25. ans[i][j] = (ans[i][j] + num[i][k] * b.num[k][j] % MOD) % MOD;
  26. }
  27. }
  28. }
  29. memcpy(num, ans, sizeof(ans));
  30. }
  31. };
  32. void mi(Matrix &res, LL n) {
  33. Matrix ans;
  34. for(; n != 0; n >>= 1) {
  35. if((n & 1) == 1) {
  36. ans *= res;
  37. }
  38. res *= res;
  39. }
  40. memcpy(res.num, ans.num, sizeof(ans.num));
  41. }
  42. int main() {
  43. LL n;
  44. Matrix matrix;
  45. scanf("%lld", &n);
  46. matrix.Set();
  47. mi(matrix, n - 1);
  48. printf("%lld", matrix.num[0][0] % MOD);
  49. return 0;
  50. }

可以看到,这里的 mi 函数与上面快速幂的写法几乎完全相同,只是定义了矩阵的结构体和矩阵乘法而已,当然矩阵乘法的写法肯定是要像能够默下来那样熟练,最好快速幂的写法也能默得下来,毕竟应该算是一种基础的算法,题目才不会这样告诉你:请用矩阵快速幂解决这道动态规划问题。

矩阵快速幂应用

说了这么多,就来检查一下是否理解了矩阵快速幂吧,最好看完下面的题目之后能够自己先想出怎么运用矩阵快速幂,再来看题解。

Okabe and El Psy Kongroo

题目链接

Codeforces 821E: Okabe and El Psy Kongroo

题意

有一个人要出去散步,但是他只能在安全的区域内散步,将他的散步区域限定在平面直角坐标系中的第一象限,从坐标 开始,往右走到 的位置,问安全的走法有多少种。
其中安全的区域有 段,每段用 三个数表示:从 之间,只能在 之间散步。已知他只能从坐标 走到 三个坐标上。
数据保证 ,且当 时,。由于结果可能非常大,将求得的结果对 取模。

题解

这题是不是与上面的青蛙跳台阶很像?如果熟练的话可以看得出来,这是一个可以用 dp 优化的搜索的题目,从走路的方式很容易看出,如果设走到坐标 的走法为 ,则:


其中任何一项的纵坐标超过 ,其值都为
递推公式有了,不过这是一个二维的 dp,并不像前面那个一维的容易写出矩阵(当然要构造矩阵啦, 的范围可是 次方,横坐标一个一个推过去肯定超时),这题可以将每一个横坐标对应的所有点看作是一个状态,那么对于每一段(注意这里每一段对应的都是不同的矩阵)的状态转移方程就是:

这样这个矩阵就容易推导多了:

最后两个细节上的问题,一个是在两段的分界线上, 值不同应当将两个 值中最小的那个 值以上的所有状态都设为 。另一个是算法的时间复杂度,显然被分的段数越多, 的取值范围越大,算法的时间复杂度也越高,整体的时间复杂度大概为 ,庆幸 也只在 左右。

过题代码

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cstdlib>
  4. #include <cmath>
  5. #include <sstream>
  6. #include <cstring>
  7. #include <string>
  8. #include <vector>
  9. #include <list>
  10. #include <queue>
  11. #include <stack>
  12. #include <map>
  13. #include <set>
  14. #include <algorithm>
  15. using namespace std;
  16. #define LL long long
  17. const int maxn = 16;
  18. const LL MOD = 1000000007;
  19. struct Matrix {
  20. LL num[maxn][maxn];
  21. Matrix() {
  22. memset(num, 0, sizeof(num));
  23. for(int i = 0; i < maxn; ++i) {
  24. num[i][i] = 1;
  25. }
  26. }
  27. void Set(int Size) {
  28. memset(num, 0, sizeof(num));
  29. for(int i = 0; i < Size; ++i) {
  30. for(int j = i - 1; j <= i + 1; ++j) {
  31. if(j >= 0 && j < Size) {
  32. num[i][j] = 1;
  33. }
  34. }
  35. }
  36. }
  37. void mult(const Matrix &b, const int &Size) {
  38. LL ans[maxn][maxn];
  39. for(int i = 0; i < Size; ++i) {
  40. for(int j = 0; j < Size; ++j) {
  41. ans[i][j] = 0;
  42. for(int k = 0; k < Size; ++k) {
  43. ans[i][j] = (ans[i][j] + num[i][k] * b.num[k][j] % MOD) % MOD;
  44. }
  45. }
  46. }
  47. memcpy(num, ans, sizeof(ans));
  48. }
  49. };
  50. LL N, K, a, b, c, Ans[2][maxn], now;
  51. Matrix tmp;
  52. void mi(Matrix &res, LL n, int Size) {
  53. Matrix ans;
  54. for(; n != 0; n >>= 1) {
  55. if((n & 1) == 1) {
  56. ans.mult(res, Size);
  57. }
  58. res.mult(res, Size);
  59. }
  60. memcpy(res.num, ans.num, sizeof(ans.num));
  61. }
  62. void setZero(LL *num, int n) {
  63. for(int i = n + 1; i < maxn; ++i) {
  64. num[i] = 0;
  65. }
  66. }
  67. int main() {
  68. #ifdef LOCAL
  69. freopen("test.txt", "r", stdin);
  70. #endif // LOCAL
  71. ios::sync_with_stdio(false);
  72. Ans[now][0] = 1;
  73. scanf("%I64d%I64d", &N, &K);
  74. while(N--) {
  75. scanf("%I64d%I64d%I64d", &a, &b, &c);
  76. if(a < K) {
  77. if(b > K) {
  78. b = K;
  79. }
  80. setZero(Ans[now], c);
  81. tmp.Set(c + 1);
  82. mi(tmp, b - a, c + 1);
  83. for(int i = 0; i <= c; ++i) {
  84. Ans[!now][i] = 0;
  85. for(int j = 0; j <= c; ++j) {
  86. Ans[!now][i] = (Ans[!now][i] + tmp.num[i][j] * Ans[now][j] % MOD) % MOD;
  87. }
  88. }
  89. now = !now;
  90. setZero(Ans[now], c);
  91. }
  92. }
  93. printf("%I64d\n", Ans[now][0]);
  94. return 0;
  95. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注