如何使用矩阵乘法加速动态规划——以[SDOI2009]HH去散步为例
题目描述
Description
HH有个一成不变的习惯,喜欢饭后百步走。所谓百步走,就是散步,就是在一定的时间 内,走过一定的距离。 但是同时HH又是个喜欢变化的人,所以他不会立刻沿着刚刚走来的路走回。 又因为HH是个喜欢变化的人,所以他每天走过的路径都不完全一样,他想知道他究竟有多 少种散步的方法。 现在给你学校的地图(假设每条路的长度都是一样的都是1),问长度为t,从给定地 点A走到给定地点B共有多少条符合条件的路径
Input
第一行:五个整数N,M,t,A,B。其中N表示学校里的路口的个数,M表示学校里的 路的条数,t表示HH想要散步的距离,A表示散步的出发点,而B则表示散步的终点。 接下来M行,每行一组Ai,Bi,表示从路口Ai到路口Bi有一条路。数据保证Ai = Bi,但 不保证任意两个路口之间至多只有一条路相连接。 路口编号从0到N − 1。 同一行内所有数据均由一个空格隔开,行首行尾没有多余空格。没有多余空行。 答案模45989。
Output
一行,表示答案。
Sample Input
4 5 3 0 0
0 1
0 2
0 3
2 1
3 2
Sample Output
4
HINT
对于30%的数据,N ≤ 4,M ≤ 10,t ≤ 10。 对于100%的数据,N ≤ 20,M ≤ 60,t ≤ 2^30,0 ≤ A,B
对这个题目的最初理解
开始看到这个题,觉得很水,直接写了一个最简单地动态规划,就是定义
f[i][j]为到了i节点路径长度为j的路径总数,
转移的话使用Floyd算法的思想去转移,借助这个题目也理解了为什么floyd要把k放在最外面,也是类似的道理。
这样就写了下面代码中的version1。但是连样例也无法通过。
我又重新仔仔细细读了一遍题,发现不可以走回头路。
然后我就一直在考虑如何避免走回头路,但是想了一个小时,也想不出一个合理的猜想,每一个猜想有非常大的局限性。
然后就上网翻题解,发现可以使用边来定义状态,进行转移。
这样就写了第二个version,得到了30分,tle。
又仔细看了一下题目,发现:t<=\(2^{30}\)!如此庞大的数据,我的\(O(n^3)\)的超级朴素算法根本无法通过。
于是上网看题解,发现了矩阵优化这一玄学的技巧。
什么是矩阵乘法
矩阵优化dp这个问题很早之前就听说过,最开始是在快速求解fibonacci数列时知道的。
我去学堂在线上上了几节线性代数课程,大概了解了矩阵乘法。
定义矩阵A(nm), 矩阵B(mk),定义矩阵C是一个(nk)的矩阵,为AB
对于矩阵C中的每一个元素,为\(\sum (a[i][k]*b[k][j])\);
这就是矩阵乘法。
矩阵乘法有什么应用呢?一大应用就是去表示线性方程组。
比如有一个方程组:
\left\{
\begin{array}{lll}
a_1x_1+a_2x_2+...+a_nx_n = z_1\\
b_1x_1+b_2x_2+...+b_nx_n = z_2 \\
...
\end{array}
\right.
\end{equation}
\]
我们写出他的系数矩阵A:
A=
\left[
\begin{array}{cccc}
a_1 & a_2 & ... & a_n \\
b_1 & b_2 & ... & b_n \\
....&...&...&...
\end{array}
\right]
\end{equation}
\]
写出增广系数矩阵B
B=
\left[
\begin{array}{c}
z_1&
z_2&
...&
z_n
\end{array}
\right]
\end{equation}
\]
写出未知数矩阵X
X=
\left[
\begin{array}{c}
x_1&
x_2&
...&
x_n
\end{array}
\right]
\end{equation}
\]
根据矩阵乘法的定义,我们可以用AX=B来表示这个线性方程组。
所以矩阵乘法可以被认为是把一个列向量向另外一个列向量的映射变化。
矩阵乘法的应用---几个dp问题
所以在动态规划中,当我们需要求解一个阶段划分明显,一个阶段可以由一个函数整体映射到下一个阶段时,我们就可以使用矩阵乘法进行优化。
我们看两个经典的dp问题,观察他们是否符合我们说明的这个规律。
首先是经典的0-1背包问题,我们写出方程:
f[i][j] = max(f[i-1][j-w[i]]+v[i], f[i-1][j]);
很显然,在前一个状态和后一个状态之间,并不是一个线性关系。
再看LIS问题:
f[i] = max(f[j]+1);
这就更不是一种线性关系了。
使用矩阵乘法加速fibonacci数列的求解
我们再看经典的fibonacci数列:
\left\{
\begin{array}{c}
f[n] = f[n-1] + f[n-2] \\
f[n-1] = f[n-1]
\end{array}
\right.
\end{equation}
\]
很显然,这就是一个线性关系;
我们可以写出这个线性方程组对应的映射:
\left[
\begin{array}{c}
1&1\\
1&0
\end{array}
\right]
\end{equation}
\]
然后可以有:
A=
\left[
\begin{array}{c}
f[n-1]&f[n-2]
\end{array}
\right]
\end{equation}
\]
B=
\left[
\begin{array}{c}
1&1\\1&0
\end{array}
\right]
\end{equation}
\]
C=
\left[
\begin{array}{c}
f[n]&f[n-1]
\end{array}
\right]
\end{equation}
\]
有\(A*B = C\)
所以我们只需要计算矩阵A*B就可以计算出C
现在我们知道
\left[
\begin{array}{c}
f[1]&f[2]
\end{array}
\right]
\end{equation}
\]
等于
\left[
\begin{array}{c}
1&1
\end{array}
\right]
\end{equation}
\]
所以\(C = A * B ^ {n-1}\)
答案就是C[1][1]。
上面便是使用矩阵乘法解决fibonacci数列求解的一个例子。
回到这个问题
现在,我们考虑这个题。
方程容易写出:
\]
这里,我们使用邻接表存储,边从2开始编号,这样一个边i的反向边就是i^1
可以看出,前一个阶段到新一个阶段的映射是线性的,并且这个映射是不变的。
我们可以预先求解这个映射。
根据矩阵乘法的定义,这个映射可以被表示成一个0-1矩阵(n*n)
对于位置i,j如果满足to[i] == from[j] && i != j^1这个条件,那么这个位置是1,反之就是0;
这样我们只需要求解出映射的(K-1)幂就可以了。
至于初始矩阵,如果某个点可以一步到达,那么这个点为1,否则为0.
这样,我们就圆满的解决了这个问题。
代码
下面上代码。
/*#include <bits/stdc++.h> //////version 1 : 10 WA
using namespace std;
const int maxn = 11;
int g[maxn][maxn];
int n, m, k, s, t;
int f[maxn][maxn];
int main() {
scanf("%d %d %d %d %d", &n, &m, &k, &s, &t);
memset(g, 0, sizeof(g));
for(int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x][y] = g[y][x] = 1;
}
memset(f, 0, sizeof(f));
f[s][0] = 1;
for(int kk = 0; kk <= k; kk++) {
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
if(g[i][j]) {
bool ok = (g[i][j]) && (f[j][kk-1]) && f[i][kk];
if(i!= j)if(!ok)f[j][kk+1] += f[i][kk];
else f[j][kk+1] = f[j][kk+1]+f[i][kk]-f[j][kk-1];
}
}
}
}
for(int i = 0; i < n; i++) {
cout << i << endl;
for(int kk = 0; kk <= k; kk++) {
cout << f[i][kk] << ' ';
}
cout << endl;
}
printf("%d", f[t][k]);
return 0;
}*/
/* ////////////////////////////version 2 : 30 TLE/RE
#include <bits/stdc++.h>
using namespace std;
const int maxn = 11;
int cnt = 0;
int to[maxn], last[maxn], nex[maxn], from[maxn];
int n, m, K, s, t;
int f[maxn][maxn];
vector<int> ans;
void insert(int fr, int tt) {
cnt++;
nex[last[fr]] = cnt;
last[fr] = cnt;
to[cnt] = tt;
from[cnt] = fr;
if(fr == s) {
f[cnt][1] = 1;
}
if(tt == t) {
ans.push_back(cnt);
}
}
bool ok(int a, int b) { //judge if it's ok to transfer f[a][k] to f[b][k+1];
return to[a] == from[b] && !(from[a] == to[b]);
}
int main() {
scanf("%d %d %d %d %d", &n, &m, &K, &s, &t);
memset(f, 0, sizeof(f));
memset(last, 0, sizeof(last));
for(int i = 0; i < m; i++) {
int a, b;
scanf("%d %d", &a, &b);
insert(a, b);
insert(b, a);
}
for(int k = 1; k <= K; k++) {
for(int i = 1; i <= cnt; i++) {
for(int j = 1; j <= cnt; j++) {
if(ok(i, j)) {
f[j][k+1] += f[i][k];
}
}
}
}
vector<int>::iterator it;
int an = 0;
for(it = ans.begin(); it != ans.end(); it++) {
an = (an+f[*it][K]) % 45989;
}
printf("%d", an);
return 0;
}*/
#include <bits/stdc++.h>
using namespace std;
const int mod = 45989;
const int maxn = 21;
int cnt = 1;
int last[maxn], nex[1005], to[1005], from[1005];
int n, m, K, s, t;
void insert(int a, int b) {
cnt++;
nex[cnt] = last[a];
last[a] = cnt;
to[cnt] = b;
from[cnt] = a;
}
struct Matrix {
int v[125][125];
Matrix(){
memset(v, 0, sizeof(v));
}
friend void print(Matrix a) {
for(int i = 1; i <= cnt; i++) {
for(int j = 1; j <= cnt; j++) {
cout << a.v[i][j] << ' ';
}
cout << endl;
}
}
friend Matrix operator * (Matrix a, Matrix b) {
Matrix ans;
for(int i = 1; i <= cnt; i++) {
for(int j = 1; j <= cnt; j++) {
for(int k = 1; k <= cnt; k++) {
ans.v[i][j] = (ans.v[i][j] + a.v[i][k] * b.v[k][j]) % mod;
}
}
}
return ans;
}
friend Matrix operator ^ (Matrix a, int b) {
Matrix ans;
for(int i = 1; i <= cnt; i++) ans.v[i][i] = 1;
for(int i = b; i;i >>= 1, a = a * a) {
if(i & 1) ans = ans * a;
}
return ans;
}
}a, b;
int main() {
scanf("%d %d %d %d %d", &n, &m, &K, &s, &t);
memset(last, 0, sizeof(last));
for(int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
insert(x, y);
insert(y, x);
}
for(int i = last[s]; i; i = nex[i]) {
a.v[1][i] = 1;
}
for(int i = 2; i <= cnt; i++) {
for(int j = 2; j <= cnt; j++) {
if(to[i] == from[j]) {
if(i != (j ^ 1)) {
b.v[i][j] = 1;
}
}
}
}
a = a * (b ^ (K-1));
/*for(int i = 1; i <= 3; i ++) {
for(int j = 2; j <= cnt; j++) {
cout << a.v[1][j] << ' ';
}
cout << endl;
a = a * b;
}*/
int ans = 0;
for(int i = last[t]; i; i = nex[i]) {
ans = ans + a.v[1][i^1];
}
printf("%d\n", ans % mod);
return 0;
}
注
1.代码实现时出了一些问题:边存储时由于拆成了两条边,所以65是不够的。
如何使用矩阵乘法加速动态规划——以[SDOI2009]HH去散步为例的更多相关文章
- BZOJ_1875_[SDOI2009]HH去散步_矩阵乘法
BZOJ_1875_[SDOI2009]HH去散步_矩阵乘法 Description HH有个一成不变的习惯,喜欢饭后百步走.所谓百步走,就是散步,就是在一定的时间 内,走过一定的距离. 但 是同时H ...
- bzoj 1875: [SDOI2009]HH去散步 -- 矩阵乘法
1875: [SDOI2009]HH去散步 Time Limit: 20 Sec Memory Limit: 64 MB Description HH有个一成不变的习惯,喜欢饭后百步走.所谓百步走, ...
- BZOJ 1875: [SDOI2009]HH去散步( dp + 矩阵快速幂 )
把双向边拆成2条单向边, 用边来转移...然后矩阵乘法+快速幂优化 ------------------------------------------------------------------ ...
- 「 洛谷 」P2151 [SDOI2009]HH去散步
小兔的话 欢迎大家在评论区留言哦~ HH去散步 题目限制 内存限制:125.00MB 时间限制:1.00s 标准输入 标准输出 题目知识点 动态规划 \(dp\) 矩阵 矩阵乘法 矩阵加速 矩阵快速幂 ...
- AC日记——[SDOI2009]HH去散步 洛谷 P2151
[SDOI2009]HH去散步 思路: 矩阵快速幂递推(类似弗洛伊德): 给大佬跪烂-- 代码: #include <bits/stdc++.h> using namespace std; ...
- bzoj1875: [SDOI2009]HH去散步
终于A了...早上按自己以前的写法一直WA.下午换了一种写法就A了qwq #include<cstdio> #include<cstring> #include<iost ...
- 洛谷P2151 [SDOI2009] HH去散步 [矩阵加速]
题目传送门 HH去散步 题目描述 HH有个一成不变的习惯,喜欢饭后百步走.所谓百步走,就是散步,就是在一定的时间 内,走过一定的距离. 但是同时HH又是个喜欢变化的人,所以他不会立刻沿着刚刚走来的路走 ...
- [LuoguP2151][SDOI2009]HH去散步_递推_矩阵乘法_图论
HH去散步 题目链接:https://www.luogu.org/problem/P2151 数据范围:略. 题解: 数据范围好小,让人不禁想用一些毒瘤算法,但是失败了. 这种类似时间啊这种有点重复味 ...
- Luogu 2151 [SDOI2009]HH去散步
BZOJ 1875 矩阵乘法加速递推. 如果不要求不能走同一条边,那么直接构造出矩阵快速幂即可,但是不走相同的道路,怎么办? 发现边数$m$也很小,我们直接把$2 * m$开成一个矩阵,相当于记录上一 ...
随机推荐
- 配置 php-fpm 监听的socket
一般现在我们配置的PHP的web环境,如LNMP(linux+Nginx+Mysql+PHP), 这里linux可能是centos, ubuntu..., 数据库可能是mysql, postgresq ...
- fuel健康检查Heat失败的原因
service openstack-heat-engine restart chkconfig --level 2345 openstack-heat-engine on
- 2016"百度之星" - 资格赛(Astar Round1) Problem D
排个序,map直接搞. #include <stdio.h> #include <math.h> #include<cstring> #include<cma ...
- webpack.config.js 参数简单了解
webpack.config.js文件通常放在项目的根目录中,它本身也是一个标准的Commonjs规范的模块. var webpack = require('webpack'); module.exp ...
- 黑科技--位集--bitset
自从上次网赛发现这么个东西之后,深深地感受到了bitset的强大,0.0. 正常的bool占用1字节空间,bitset可以把这个缩到1bit,空间上8倍优化.正常用起来可能会跟位运算状态压缩类似,但是 ...
- CodeForces 618B Guess the Permutation
只要找出当前没用过的数字中,大于或等于当前这一列的最大值就可以 #include<cstdio> #include<cstring> #include<cmath> ...
- (中等) HDU 4725 The Shortest Path in Nya Graph,Dijkstra+加点。
Description This is a very easy problem, your task is just calculate el camino mas corto en un grafi ...
- STM32的NVIC_PriorityGroupConfig使用及优先级分组方式理解
STM32的NVIC_PriorityGroupConfig使用及优先级分组方式理解 STM32的优先级NVIC_PriorityGroupConfig的理解及其使用 STM32中断优先级彻底讲解 S ...
- 让程序同时只能运行一个C++ Builder实现(转)
源:让程序同时只能运行一个 很多人都讨论过这个问题, 这里用Victor串口控件里面现成的共享内存功能来实现. 当程序运行第二次时只是激活第一次运行的窗口, 而不是再运行一个程序. 需要在主程序里实现 ...
- 使用LIBUSB实现和自定义通讯设备通讯--MFC代码在末尾
LIBUSB是一款简单好用的USB通讯开发库,一般HID设备用该库通讯能大大降低开发周期,使用如下,首先需要为设备安装驱动 在libusb的bin目录下有一个inf_wirzed.exe的文件,该文件 ...