矩阵快速幂优化DP
wjyppm1403 · · 算法·理论
可能更好的阅读体验
0. 前言
蒟蒻做题比较少,在做过的题中选出了一些经典的例题与技巧帮助大家,这篇文章也只是我个人的一个经验总结,希望能帮助到后人学习。
1.矩阵小芝士
矩阵优化是干啥的啊?
有的时候,你会发现你设计了一个极好的 DP 状态,没有后效性,没有重叠,你很开心,你去看数据范围就会炸掉!你死活都想不出来怎么优化,感觉去掉这个状态之后就感觉 “不完美” 了,让后点开题解,发现一堆密密麻麻的数学公式和矩阵,开心的点进去,郁闷的叉掉,那么怎么解决这种郁闷心情呢?当然就是学矩阵优化啦。
好端端的那么优美的递推式子我们为什么要用矩阵来转移呢?答案很简单,因为矩阵乘法有结合律,可以快速幂!当转移式子固定的时候,我们可以通过快速幂,设置好初始矩阵和转移矩阵,通过矩阵快速幂就能轻轻松松的将复杂度变成
我们介绍一下矩阵乘法:
设
即
向量可以视为
矩阵乘法满足结合律和分配律,但不满足交换律。
不只是普通乘法,我们还有广义矩阵乘法!
一些约定:
若
广义矩阵乘法也同样满足普通矩阵乘法的结合律、分配律。
例如
广义上说,只要我们内层的运算具有结合律,外层的运算对于内层运算有分配律,并且状态转移递推恒定,并且发现转移来的状态数少但转移次数极大,我们就可以考虑矩阵优化 DP。
严谨的讲,其实矩阵优化的本质就是线性递推 DP 方程,也就是从
设矩阵规模为
我们这里给出一个封装好的矩阵,其中 MN 需要额外定义常量。
struct Matrix{
int mat[MN][MN];
Matrix(int x=0){
memset(mat,0,sizeof(mat));
if(!x) return;
for(int i=0;i<MN;i++) mat[i][i]=x;
}
Matrix operator*(const Matrix x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
ret.mat[i][j]+=mat[i][k]*x.mat[k][j];
}
}
}
return ret;
}
};
Matrix ksm(Matrix a,int b){
Matrix ret(1);
while(b){
if(b&1) ret=ret*a;
a=a*a;
b>>=1;
}
return ret;
}
2.矩阵优化操作手册
Luogu P1962 斐波那契数列
斐波那契数列是满足如下性质的一个数列:
请你求出
这个题看起来还挺简单的,不就是斐波那契数列递推吗,直接
但是
我们发现,如果你想递推出
这里解释一下,转移来的状态指的是能够给当前状态贡献答案的状态。
通过简单的例题我们来介绍以下矩阵优化的 “操作手册”。
写出转移方程,判断优化类型
对于一般的矩阵优化方程,它的转移方程一般是不难写出来的,但是你会通常会发现其中一个维度数量极其大,转移十分困难,空间也十分困难,而且一个特征就是转移的式子很简单,近似于线性递推而且转移来的状态数量极少,这种就是矩阵优化的鲜明特征。
这里我们的方程就是:
然而有的时候你可能看不出来维度数量极其大,这种我的建议就是你在设计 DP 的时候要写全,就是方程写出来,初始化设置好,计算好空间。
确定优化维度
我们要确定我们是要对哪一维度,普遍都是对那些数极其大的状态维度进行优化,有的时候可能不太一样,需要自行判断。
这里的优化维度显然就是
根据转移方程需要的量,确定初始矩阵
确定初始矩阵实际上就是我们到底在转移需要什么数据才能转移到
注意到,我们的转移方程需要的是前两个数,于是我们的初始矩阵可以这么设置:
你这不对吧,
事实上,你应当回忆第一节后面我们讲的,矩阵优化的本质就是线性递推 DP 方程。我们从
那么初始设置就是
初始矩阵的实质?它是动态规划中递推起点的状态值所构成的矩阵,必须包含递推所需的全部初始状态,只有包含这些初始状态才能推到后面的状态。
一般来说,我们的初始矩阵大多是是一个向量的形式存在,我们的初始矩阵其实就是用来存状态的。
大多数时候初始矩阵的如果不填则默认为 0,但有的时候因为运算的限制我们需要填写负无穷(例如出现负数的时候或者取
设计转移矩阵
难点来了,我们考虑如何设计转移矩阵。
我们回忆一下矩阵乘法的操作:
图画的不好,请见谅,但大体上是这个操作,我们考虑我们的转移方程该怎么操作。
首先明确目标,我们是要从
接下来我们确定转移后的数据,转移后我们要递推
接下来是填空时间!请读者自行填空。若感到吃力可以看上面我们矩阵运算的示意图。答案下面会给出
我们总结一下这个步骤:
- 确定转移矩阵大小。
- 确定初始矩阵转移后的目标矩阵。
- 根据转移方程和目标矩阵,列出转移矩阵。
- 利用转移矩阵模拟运算后是否能得出目标矩阵的结果。
答案如下:
确定转移顺序,确定快速幂的幂数
我们不妨设初始矩阵为
那么求
注意到一开始说矩阵乘法是有结合律的,考虑直接把后面结合起来,注意到是一个幂的形式,于是我们就可以快速幂了!
那为什么需要这一步呢,是因为有的时候转移过程中因为题目条件矩阵转移顺序有所变化,所以我们可能需要中断转移改变转移矩阵,这里下面的例题会讲到。
在这里我们需要确定矩阵快速幂的幂数,注意到我们初始矩阵是
根据初始矩阵设计,确定答案统计范围
这个一般需要你通过初始矩阵设计来确定,可能是矩阵的某一项,或者矩阵一个范围。
写代码,注意细节。
写代码的时候尤其要注意矩阵初始化的赋值,到底是 0 还是初始化为正无穷还是负无穷。注意矩阵的数据大小选择合适的存储类型。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MOD=1e9+7,MT=5;
struct matrix{
ll mt[MT][MT]{};
matrix operator *(const matrix &x){
matrix ret;
for(int i=1;i<=2;i++){
for(int j=1;j<=2;j++){
for(int k=1;k<=2;k++){
ret.mt[i][j]+=mt[i][k]*x.mt[k][j]%MOD;
// ret.mt[i][j]%=MOD;
}
}
}
return ret;
}
};
ll n;
matrix qp(matrix x,matrix ans,ll k){
while (k)
{
if(k&1){
ans=ans*x;
}
x=x*x;
k>>=1;
}
return ans;
}
matrix ans,base;
int main(){
cin>>n;
if(n<2){
cout<<n;
return 0;
}
base.mt[1][1]=base.mt[1][2]=base.mt[2][1]=1;
ans.mt[1][1]=ans.mt[1][2]=1;
cout<<qp(base,ans,n-2).mt[1][1]%MOD;
return 0;
}
3. 例题与技巧
接下来是例题时间,对于每一个例题我们后面都会有对应的技巧进行讲解。
USACO07NOV Cow Relays G——Floyd倍增转移
给定一张
T 条边的无向连通图,求从S 到E 经过N 条边的最短路长度。
你真的理解 Floyd 了吗?
我们回忆图的邻接矩阵,邻接矩阵的本质是矩阵,它原始表示图两点之间经过
我们考虑 Floyd 算法本质上是在干什么,对于 3 层循环 mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]);。你会发现,这有点类似于广义矩阵乘法啊!
事实上,Floyd 枚举中间点的转移,事实上对于原来的邻接矩阵来说,从经过 1 条边到 2 条边,从经过 2 条边到经过 3 条边,如此下去将所有边遍历完就能求出多源最短路了!
以下我们改写以下方程:
根据 Floyd,
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=520;
int n,t,s,e,tot;
struct Matrix{
int mt[MN][MN];
Matrix operator*(const Matrix &x)const{
Matrix ans;
memset(ans.mt,0x3f,sizeof(ans.mt));
for(int k=1;k<=tot;k++){
for(int i=1;i<=tot;i++){
for(int j=1;j<=tot;j++){
ans.mt[i][j]=min(ans.mt[i][j],mt[i][k]+x.mt[k][j]);
}
}
}
return ans;
}
}dis,ans;
unordered_map<int,int> ump;
Matrix ksm(){
n--;
ans=dis;
while(n){
if(n&1){
ans=ans*dis;
}
dis=dis*dis;
n>>=1;
}
return ans;
}
int main(){
cin>>n>>t>>s>>e;
memset(dis.mt,0x3f,sizeof(dis.mt));
for(int i=1;i<=t;i++){
int u,v,w;
cin>>w>>u>>v;
if(!ump[u]) ump[u]=++tot;
if(!ump[v]) ump[v]=++tot;
dis.mt[ump[u]][ump[v]]=dis.mt[ump[v]][ump[u]]=w;
}
cout<<ksm().mt[ump[s]][ump[e]];
return 0;
}
经典图例题
给定一个
n 个点,m 条边的有向图,对于两个点之间要么不连边,要么边权为1 ,求从起点出发长度为k 到达终点的路径方案数(n \le 500,m\le 250 ,k 极大)。
先不考虑数据范围,我们可以用 DP 来解决。
设
注意到
SCOI2009迷路——边权拆点
给定一个
n 个点,m 条边的有向图,边权为w ,求从 1 号点出发长度为k 到达n 号点的路径方案数(n \le 10,w \le 9 ,k\le 10^9 )。
注意到这里边权为
这里我们用到一个技巧叫做边权拆点,矩阵的要求就是线性递推,那么为了满足线性递推的要求,我们把一条边拆成一个一个边权为 1 的小边,同时我们把一个节点拆成 9 个点,分别表示周围边权为 1 到周围边权为 9。对于拆点内部,我们让
使用这个技巧的的前提是边权很小,这样我们才能用边权拆点。
拆点实际上就是将原图换为一个于此等价的图,使得题目中的特殊性质被满足,从而达到简化题目的目的。
我们其实也可以拆边,把边拆为点,但是我们应当在尽量满足题意的情况下尽量减少点的数量,从而减低复杂度,时间复杂度为
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=100,MOD=2009;
int n,t,tot,idx[MN][10];
struct Matrix{
int mat[MN][MN];
Matrix(int x=0){
memset(mat,0,sizeof(mat));
if(!x) return;
for(int i=0;i<MN;i++) mat[i][i]=x;
}
Matrix operator*(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
ret.mat[i][j]+=mat[i][k]*x.mat[k][j];
ret.mat[i][j]%=MOD;
}
}
}
return ret;
}
}A,G;
Matrix ksm(Matrix a,int b){
Matrix ret(1);
while(b){
if(b&1) ret=ret*a;
a=a*a;
b>>=1;
}
return ret;
}
void initG(){
for(int i=1;i<=n;i++){
for(int j=1;j<=9;j++){
idx[i][j]=++tot;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<9;j++){
G.mat[idx[i][j]][idx[i][j+1]]=1;
}
}
}
int main(){
cin>>n>>t;
initG();
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
char c;
int num;
cin>>c;
num=c-'0';
if(num>0){
G.mat[idx[i][num]][idx[j][1]]=1;
}
}
}
A=ksm(G,t);
cout<<A.mat[1][idx[n][1]]%2009;
return 0;
}
SDOI2009 HH去散步——点边转化,超级源点
给定
n 个点,m 条无向边的图,边权均为 1,求从S\rightarrow T 的路径上有多少长度为t 的路径,满足上一步走过的路下一步不能重复走。答案对45989 取模。
考虑DP,设
那你这方程也不对啊,题目说了满足上一步走过的路下一步不能重复走。那么接下来怎么在图上转移?
卡住了……但是我们发现边数极小!当然我们需要构造一个新图使得能够满足这个限制,考虑怎么构造?
注意到边数极小,我们可以利用点边互换的技巧!点边互换的核心思想是:把原图中的某些点看作边,或者把原图中的某些边看作点 ,从而构造一个新的图来满足题目中的限制。
边可以转为点,而点也可以转为边,这样的代价就是我们必须增加点的数量或边的数量。
那么我们修改一下转移方程,转移方程基本不变,但是转移有区别。
其中
但是
对于每一个点,期望连边 2 条,都会割出 2 个点,最多
割点其实就是将每个点的不同状态放大化,使其“人格分裂”,从而将【节点的转移】转化为【状态的转移】,从而达到实现满足特殊条件约束的目的。 ——Ydtz 的教程
HAOI2015 数字串拆分——指数高精?发掘性质!
自己看题吧。
注意到
初始
注意到
但是你会发现这个
考虑分析
正确性是很显然的,例如
考虑按数位递推求解
答案即为
问题转化为如何求出一个后缀字符串的
当我们发现指数需要高精,心中默念 “这题绝对不是让我指数高精,一定题目中有什么其他的特性在里面,如果山穷水尽可能我们还有特殊快速幂” 关于这个特殊快速幂我们下文会提到。
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=5,MOD=998244353;
int m,len,ans;
string s;
struct Matrix{
int mat[MN][MN];
Matrix(int x=0){
memset(mat,0,sizeof(mat));
if(x==0) return;
for(int i=0;i<MN;i++){
mat[i][i]=x;
}
}
void init(int x){
memset(mat,0,sizeof(mat));
if(x==0) return;
for(int i=0;i<MN;i++){
mat[i][i]=x;
}
}
void initf(){
for(int i=0;i<m;i++) mat[i][m-1]=1;
for(int i=1;i<m;i++) mat[i][i-1]=1;
}
Matrix operator*(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
ret.mat[i][j]+=mat[i][k]*x.mat[k][j]%MOD;
ret.mat[i][j]%=MOD;
}
}
}
return ret;
}
Matrix operator+(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
ret.mat[i][j]=(mat[i][j]+x.mat[i][j])%MOD;
}
}
return ret;
}
}pw[520],f[520][520],g[520];
Matrix ksm(Matrix a,int b){
Matrix ret(1);
while(b>0){
if(b&1) ret=ret*a;
a=a*a;
b>>=1;
}
return ret;
}
signed main(){
cin>>s>>m;
len=s.length();
s=' '+s;
pw[0].initf();
for(int i=1;i<len;i++) pw[i]=ksm(pw[i-1],10);
for(int i=1;i<=len;i++){
for(int j=i;j>=1;j--){
if(i!=j){
f[j][i]=f[j+1][i]*ksm(pw[i-j],s[j]-'0');
}else{
f[j][i]=ksm(pw[0],s[j]-'0');
}
}
}
g[0].init(1);
for(int i=1;i<=len;i++){
for(int j=0;j<i;j++){
g[i]=g[i]+(g[j]*f[j+1][i]);
}
}
for(int i=0;i<m;i++) ans=(ans+g[len].mat[0][i])%MOD;
cout<<ans;
return 0;
}
NOI2013 矩阵游戏——指数高精与费马小定理,10进制快速幂。
不难发现是线性递推,考虑设置两个转移方程
转移方程已经写出来了,我们考虑怎么设置初始转移,我们不难发现这个矩阵转移常数项怎么处理,对于常数项的处理我们一般是在初始矩阵中加一位列单独设置为 1,让后在之后的转移一直设置为 1,如果需要常数项我们就将对应位置改成
矩阵如下,列转移:
行转移:
对于转移实际上就是
注意到
等会
显然我没写 10进制快速幂,心中默念是不是有没有什么性质。
注意到这个矩阵对于列的转移,其中
注意到,当
#include<bits/stdc++.h>
#define int __int128
using namespace std;
constexpr int MN=15,MOD=1e9+7;
int n,m,a,b,c,d,base0,base1;
string sn,sm;
struct Matrix{
int mat[MN][MN];
// INIT THE MATRIX
Matrix(int x=0){
memset(mat,0,sizeof(mat));
for(int i=0;i<MN;i++) mat[i][i]=x;
}
Matrix operator*(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
ret.mat[i][j]+=mat[i][k]*x.mat[k][j];
ret.mat[i][j]%=MOD;
}
}
}
return ret;
}
}M1,M2;
Matrix ksm(Matrix A,int B){
Matrix ret(1);
while(B>0){
if(B&1) ret=ret*A;
A=A*A;
B>>=1;
}
return ret;
}
namespace ly
{
namespace IO
{
#ifndef LOCAL
constexpr auto maxn=1<<20;
char in[maxn],out[maxn],*p1=in,*p2=in,*p3=out;
#define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,maxn,stdin),p1==p2)?EOF:*p1++)
#define flush() (fwrite(out,1,p3-out,stdout))
#define putchar(x) (p3==out+maxn&&(flush(),p3=out),*p3++=(x))
class Flush{public:~Flush(){flush();}}_;
#endif
namespace usr
{
template<typename type>
inline type read(type &x)
{
x=0;bool flag(0);char ch=getchar();
while(!isdigit(ch)) flag^=ch=='-',ch=getchar();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return flag?x=-x:x;
}
template<typename type>
inline void write(type x)
{
x<0?x=-x,putchar('-'):0;
static short Stack[50],top(0);
do Stack[++top]=x%10,x/=10;while(x);
while(top) putchar(Stack[top--]|48);
}
inline char read(char &x){do x=getchar();while(isspace(x));return x;}
inline char write(const char &x){return putchar(x);}
inline void read(char *x){static char ch;read(ch);do *(x++)=ch;while(!isspace(ch=getchar())&&~ch);}
template<typename type>inline void write(type *x){while(*x)putchar(*(x++));}
inline void read(string &x){static char ch;read(ch),x.clear();do x+=ch;while(!isspace(ch=getchar())&&~ch);}
inline void write(const string &x){for(int i=0,len=x.length();i<len;++i)putchar(x[i]);}
template<typename type,typename...T>inline void read(type &x,T&...y){read(x),read(y...);}
template<typename type,typename...T>
inline void write(const type &x,const T&...y){write(x),putchar(' '),write(y...),sizeof...(y)^1?0:putchar('\n');}
template<typename type>
inline void put(const type &x,bool flag=1){write(x),flag?putchar('\n'):putchar(' ');}
}
#ifndef LOCAL
#undef getchar
#undef flush
#undef putchar
#endif
}using namespace IO::usr;
}using namespace ly::IO::usr;
signed main(){
read(sn,sm,a,b,c,d);
base0=(a==1?MOD:MOD-1);
for(auto p:sm){
m=((m<<3)+(m<<1)+(p^48))%(base0);
}
Matrix ans;
M1.mat[0][0]=a,M1.mat[0][1]=b,M1.mat[1][1]=1;
M2.mat[0][0]=c,M2.mat[0][1]=d,M2.mat[1][1]=1;
ans.mat[0][0]=ans.mat[1][0]=1;
Matrix d=ksm(M1,(m+base0-1)%base0)*M2;
int base1;
if(d.mat[0][0]==1) base1=MOD;
else base1=MOD-1;
for(auto p:sn){
n=((n<<3)+(n<<1)+(p^48))%(base1);
}
ans=ksm(d,(n+base1-1)%base1)*ksm(M1,(m+base0-1)%base0)*ans;
put(ans.mat[0][0]);
return 0;
}
CF576D Flights for Regular Customers——强制中断,bitset优化
又是特殊限制,我们还是设 DP。
设
考虑无解的情况怎么做,不妨假设 1 号节点边都可以走,如果都可以走的情况下还是到不了那就 GG。
我们根据操作手册,发现在第五步就炸了,因为每一次
我们不难发现
struct Matrix{
bitset<MN> mat[MN];
Matrix(int x=0){
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
mat[i][j]=0;
}
}
if(!x) return;
for(int i=0;i<MN;i++) mat[i][i]=x;
}
Matrix operator*(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int k=0;k<MN;k++){
if(mat[i][k]){// 把j省去了
ret.mat[i]|=x.mat[k];
}
}
}
return ret;
}
};
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=250,INF=1e18;
struct Edge{
int u,v,d;
}e[MN];
int n,m,ans,dis[MN];
struct Matrix{
bitset<MN> mat[MN];
Matrix(int x=0){
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
mat[i][j]=0;
}
}
if(!x) return;
for(int i=0;i<MN;i++) mat[i][i]=x;
}
Matrix operator*(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int k=0;k<MN;k++){
if(mat[i][k]){
ret.mat[i]|=x.mat[k];
}
}
}
return ret;
}
}a,G;
Matrix ksm(Matrix a,int b){
Matrix ret(1);
while(b>0){
if(b&1) ret=ret*a;
a=a*a;
b>>=1;
}
return ret;
}
bool cmp(Edge x,Edge y){
return x.d<y.d;
}
void bfs(){
for(int i=1;i<=n;i++) dis[i]=INF;
queue<int> q;
for(int i=1;i<=n;i++){
if(a.mat[1][i]) q.push(i),dis[i]=0;
}
while(!q.empty()){
int f=q.front();
cerr<<f<<'\n';
q.pop();
for(int i=1;i<=n;i++){
if(G.mat[f][i]&&dis[i]==INF){
dis[i]=dis[f]+1;
q.push(i);
}
}
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v>>e[i].d;
}
sort(e+1,e+1+m,cmp);
a.mat[1][1]=1,ans=INF,dis[n]=INF;
for(int i=1,lst=0;i<=m;i++){
if(ans<e[i].d) break;
int dt=e[i].d-lst;
lst=e[i].d;
a=a*ksm(G,dt);
G.mat[e[i].u][e[i].v]=1;
if(i==m||e[i+1].d!=e[i].d) bfs();
ans=min(ans,lst+dis[n]);
}
if(ans==INF) cout<<"Impossible";
else cout<<ans;
return 0;
}
NOI2020 美食家——强制中断,二进制分组快速幂!
给定一张
n 个点m 条边的有向图,有重边,边有时间w_i 。每一个点有价值c ,走到点i 可以获得c_i 的价值。 初始时间为0 ,你需要从起点1 开始走,恰好在T 时间回到起点1 。最终得到的价值为所有经过的点的价值c 的和,点可以被重复经过而且价值也可以重复累加。 不能在任何一个点停留,也就是说t 时间到达一个点t+1 时间必须往其他点走。 现在有k 个特殊时间点,形式为三个参数(t_i,x_i,y_i) 。表示恰好在t_i 时间点到达点x_i 时可以得到y_i 的额外价值。 求最终能获得的最大价值和,若无法在T 时间回到起点,输出-1 。1\le n\le 50,n\le m \le 500,0 \le k \le 200 1\le t_{i}\le T \le 10^9,1\le w_{i}\le 5,1\le c_{i}\le 52501,1\le y_{i}\le 10^9 数据保证所有
t_i 互不相同,图一定联通。
一眼 DP(因为我也想不出来如何用贪心做),考虑设
这是转移方程,我们可以写成递推的形式,就不写出了。考虑特殊时间,直接用 map 存下来特判即可,这样我们就能拿到高贵的 40 分(环特判时间即可),对于性质 A 是一个大环直接瞎做即可,就有 50 分。
首先特别重要的一点,
不对啊,还是卡在了第五步,因为有特殊时间点的存在,我们可以想上面题一样强制中断快速幂,更新矩阵后再次快速幂,ok 啊,写完,交上去发现只有 75 分(拼好分版本)。
怎么回事呢,原来是
我们考虑怎么优化,发现每次中断转移后重新转移的
我们考虑对
分析复杂度,不难发现我们的初始矩阵是一个 1 行
类似于这种凑
注意转矩阵乘法后千万不要思维定势,因为这里是取
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=260,NINF=-1e18;
struct Festival{
int t,u,y;
}fst[MN];
int logt,n,m,T,K,tot,idx[MN][MN],c[MN];
struct Matrix{
int mat[MN][MN];
Matrix(int x=NINF){
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
if(i==j) mat[i][j]=x;
else mat[i][j]=NINF;
}
}
}
Matrix operator*(const Matrix &x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
if(mat[i][k]==NINF||x.mat[k][j]==NINF) continue;
ret.mat[i][j]=max(ret.mat[i][j],mat[i][k]+x.mat[k][j]);
}
}
}
return ret;
}
}pw[55],A,B;
void initA(){
for(int i=1;i<=n;i++){
for(int j=1;j<=5;j++){
idx[i][j]=++tot;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<5;j++){
A.mat[idx[i][j]][idx[i][j+1]]=0;
}
}
}
void initpw(Matrix x){
pw[0]=x;
for(int i=1;i<=logt;i++) pw[i]=pw[i-1]*pw[i-1];
}
bool cmp(Festival x,Festival y){
return x.t<y.t;
}
Matrix ksmpw(Matrix a,int b){
for(int i=0;i<=logt;i++){
if((b>>i)&1){
Matrix ret;
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
if(a.mat[1][k]==NINF||pw[i].mat[k][j]==NINF) continue;;
ret.mat[1][j]=max(ret.mat[1][j],a.mat[1][k]+pw[i].mat[k][j]);
}
}
a=ret;
}
}
return a;
}
signed main(){
cin>>n>>m>>T>>K;
logt=__lg(T);
for(int i=1;i<=n;i++){
cin>>c[i];
}
initA();
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
A.mat[idx[u][w]][idx[v][1]]=c[v];
}
for(int i=1;i<=K;i++){
cin>>fst[i].t>>fst[i].u>>fst[i].y;
}
sort(fst+1,fst+1+K,cmp);
initpw(A);
B.mat[1][idx[1][1]]=c[1];
int lst=0;
for(int i=1;i<=K;i++){
B=ksmpw(B,fst[i].t-lst);
if(B.mat[1][idx[fst[i].u][1]]!=NINF) B.mat[1][idx[fst[i].u][1]]+=fst[i].y;
lst=fst[i].t;
}
if(lst!=T) B=ksmpw(B,T-lst);
if(B.mat[1][idx[1][1]]==NINF) cout<<-1;
else cout<<B.mat[1][idx[1][1]];
return 0;
}
POI 2015 WYC——倍增二分思想
给定一个
n 个点,m 条边的有向图,无自环有重边,边权为w ,求第k 小路径长度(1\le n \le 40,1\le m\le 1000,1 \le w \le 3 ,k\le 10^{18} )。
A* ? 痴人说梦啊。
注意到又是经典的 G.mat[idx[u][c]][idx[v][1]]++;。但是我们怎么统计一个状态有多少路径呢?我们发现可以利用超级源点的思想,0 号点向他们连边,边权为 0,这样就能够统计统计 0 号点的路径得到全局答案了。
注意到我们需要找第
注意开 __int128:
#include<bits/stdc++.h>
#define int __int128
using namespace std;
constexpr int MN=150;
int n,m,k,lgk,tot,idx[MN][5];
struct Matrix{
int mat[MN][MN];
Matrix(int x=0){
memset(mat,0,sizeof(mat));
if(!x) return;
for(int i=0;i<MN;i++) mat[i][i]=x;
}
Matrix operator*(const Matrix x)const{
Matrix ret;
for(int i=0;i<MN;i++){
for(int j=0;j<MN;j++){
for(int k=0;k<MN;k++){
ret.mat[i][j]+=mat[i][k]*x.mat[k][j];
}
}
}
return ret;
}
}a,G,pw[70];
namespace ly
{
namespace IO
{
#ifndef LOCAL
constexpr auto maxn=1<<20;
char in[maxn],out[maxn],*p1=in,*p2=in,*p3=out;
#define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,maxn,stdin),p1==p2)?EOF:*p1++)
#define flush() (fwrite(out,1,p3-out,stdout))
#define putchar(x) (p3==out+maxn&&(flush(),p3=out),*p3++=(x))
class Flush{public:~Flush(){flush();}}_;
#endif
namespace usr
{
template<typename type>
inline type read(type &x)
{
x=0;bool flag(0);char ch=getchar();
while(!isdigit(ch)) flag^=ch=='-',ch=getchar();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return flag?x=-x:x;
}
template<typename type>
inline void write(type x)
{
x<0?x=-x,putchar('-'):0;
static short Stack[50],top(0);
do Stack[++top]=x%10,x/=10;while(x);
while(top) putchar(Stack[top--]|48);
}
inline char read(char &x){do x=getchar();while(isspace(x));return x;}
inline char write(const char &x){return putchar(x);}
inline void read(char *x){static char ch;read(ch);do *(x++)=ch;while(!isspace(ch=getchar())&&~ch);}
template<typename type>inline void write(type *x){while(*x)putchar(*(x++));}
inline void read(string &x){static char ch;read(ch),x.clear();do x+=ch;while(!isspace(ch=getchar())&&~ch);}
inline void write(const string &x){for(int i=0,len=x.length();i<len;++i)putchar(x[i]);}
template<typename type,typename...T>inline void read(type &x,T&...y){read(x),read(y...);}
template<typename type,typename...T>
inline void write(const type &x,const T&...y){write(x),putchar(' '),write(y...),sizeof...(y)^1?0:putchar('\n');}
template<typename type>
inline void put(const type &x,bool flag=1){write(x),flag?putchar('\n'):putchar(' ');}
}
#ifndef LOCAL
#undef getchar
#undef flush
#undef putchar
#endif
}using namespace IO::usr;
}using namespace ly::IO::usr;
void initG(){
for(int i=1;i<=n;i++){
for(int j=1;j<=3;j++) idx[i][j]=++tot;
}
for(int i=1;i<=n;i++){
for(int j=1;j<3;j++){
G.mat[idx[i][j]][idx[i][j+1]]++;
}
G.mat[idx[i][1]][0]++;
a.mat[0][idx[i][1]]++;
}
G.mat[0][0]++;
}
signed main(){
read(n,m,k);
initG();
for(int i=1;i<=m;i++){
int u,v,c;
read(u,v,c);
G.mat[idx[u][c]][idx[v][1]]++;
}
int i;
pw[0]=G;
for(i=1;;i++){
pw[i]=pw[i-1]*pw[i-1];
Matrix ret=a*pw[i];
if(ret.mat[0][0]-n>=k) break;
if(i>62){
cout<<-1;
return 0;
}
}
int ans=0;
for(;i>=0;i--){
Matrix ret=a*pw[i];
if(ret.mat[0][0]-n<k){
a=ret;
if(ret.mat[0][0]-n<k) ans+=(1ll<<i);
}
}
put(ans);
return 0;
}
CF1067D Computer Game——矩阵优化与斜率优化,倍增二分思想
首先这个升级显然是吓唬你的,因为我可以一直选一个游戏 van,所以我只需要看
不难有 dp 方程如下,设
时间复杂度
不难看出来可以斜率优化啊,但是我们要变下形式:
因为
因为两个游戏之间获得的收益不可能比玩最大收益(最大的
故单调队列优化,时间复杂度
这个数据范围已经不行了,必须出矩阵优化....等会矩阵怎么斜率优化?
首先我们先把转移的矩阵搞出来,推啊推:
其实也不是很难推,有啥加啥,因为少个 1 直接加上去就行。
如果我们想找出来有哪些游戏是我们在斜率优化需要的,可以利用单调栈(不能用单调队列我们要存下来的)来记录我们斜率从那些点转移过来,现在问题在于如何确定什么时候从一个点转移到另一个点。
回忆一下这张图:
在斜率优化上,我们能用单调队列来做是因为对于每一个点上的斜率,它有一定转移的边界,在这之前是这个斜率,在之后就不是了。
说的好听矩阵怎么做?首先一个游戏的转移矩阵肯定不会变。问题在于我们怎么像单调队列优化一样找到所谓的边界呢?
首先单调队列不太行因为它不适用于矩阵这种玩意,那怎么办,矩阵这玩意也不能上斜率不单调三小将…………二分?
但其实维护凸壳的时候斜率函数单调递增,我们可以借助这个二分,找到顶点就可以了,其实二分也可以套在
k 与x 同单调的地方,芝士没有那么优罢了
我们可以二分矩阵快速幂的幂,到哪个幂的时候转移是最优的!这样的时间复杂度是
我们不妨快点,不难发现幂其实是一个倍增的形式,我们可以利用倍增的形式二分,首先预处理矩阵快速幂后的各个幂对应的矩阵,从大到小枚举倍增的幂,不断检查是否合法(即是否小于
代码如下,注意精度!!!!!
#include<bits/stdc++.h>
#define ll long long
#define double long double
using namespace std;
constexpr int MN=6e5+15;
constexpr double eps=1e-13;
struct Node{
double k,b;
}ln[MN],cl[MN],s[MN];
ll n,t,top,tot,now;
double v;
struct Matrix{
double mat[5][5];
Matrix operator *(const Matrix &x)const{
Matrix ret;
memset(ret.mat,0,sizeof(ret.mat));
for(int i=1;i<=3;i++){
for(int j=1;j<=3;j++){
for(int k=1;k<=3;k++){
ret.mat[i][j]+=mat[i][k]*x.mat[k][j];
}
}
}
return ret;
}
}g[40],f;
bool cmp(Node x,Node y){
if(fabs(x.k-y.k)<eps) return x.b<y.b;
return x.k<y.k;
}
int ck(double x){
if(fabs(x)<eps) return 0;
return x>0?1:-1;
}
double gety(Node x,Node y){
return (x.b-y.b)/(y.k-x.k);
}
int main(){
cin>>n>>t;
for(int i=1;i<=n;i++){
double a,b,p;
cin>>a>>b>>p;
v=max(v,b*p);
ln[i].k=p;
ln[i].b=p*a;
}
sort(ln+1,ln+1+n,cmp);
for(int i=1;i<=n;i++){
// 先把那些相等斜率的全排了
if(i == n || ck(ln[i].k - ln[i+1].k) != 0) cl[++tot]=ln[i];
}
for(int i=1;i<=tot;i++){
// 单调栈处理转移的点
while(top>1&&ck(gety(s[top],s[top-1])-gety(cl[i],s[top-1]))>=0) top--;
s[++top]=cl[i];
}
// f 为 初始矩阵
f.mat[1][3]=1;
for(int i=1;i<=top;i++){
double x=now*v-f.mat[1][1];
while(i<top&&ck(x-gety(s[i],s[i+1]))>=0) i++;// 先把过时决策排了
if(i<top) x=gety(s[i],s[i+1]);
g[0].mat[1][2]=g[0].mat[1][3]=g[0].mat[2][3]=0;
g[0].mat[2][2]=g[0].mat[3][2]=g[0].mat[3][3]=1;
g[0].mat[1][1]=1-s[i].k,g[0].mat[2][1]=s[i].k*v,g[0].mat[3][1]=s[i].b;// 初始化矩阵
for(int j=1;j<=35;j++) g[j]=g[j-1]*g[j-1];
for(int j=35;j>=0;j--){
ll np=now+(1ll<<j);
if(np>=t) continue;
// 如果决策更优或已经到头了即可转移
if(i==top||ck(x-np*v+(f*g[j]).mat[1][1])>=0){
f=f*g[j];
now=np;
}
}
f=f*g[0];
now++;
if(now==t) break;
}
cout<<fixed<<setprecision(10)<<f.mat[1][1];
return 0;
}
4.后言
总结下来,矩阵优化的特征分为如下:
- 数据范围既有极大又有极小
- 具有线性递推特征,或者转移过来的状态数量偏小。
- DP 式子可以写成矩阵并且矩阵规模小
- 转移规模极大
用途,一个优化转移,一个优化边权较小图上的统计问题,基本步骤就是根据操作手册来就行。
转移过程中还是有一些 trick 的:
- 如果题目中有一些关键节点会改变转移矩阵,考虑到向量乘矩阵和矩阵乘矩阵不同复杂度,可以考虑强制中断,二进制分组处理。
- 如果转移矩阵仅由 0/1 构成,考虑 bitset 优化。
- 若图有边权,边权极小,可以考虑拆点
- 如果涉及到指数高精,想想性质或者 10 进制快速幂
- 矩阵快速幂也满足倍增的思想,可以考虑倍增二分
个人理解,矩阵优化的本质就是线性递推,其实回看所有拆点操作,都是将这些转移满足
而对于操作手册是自己根据刷题经验总结出来的一些步骤,大家可以根据这几个步骤灵活调用。
完结撒花!