whiteqwq
2020-08-05 22:07:19
更好的阅读体验
我们看一道题:P2233 [HNOI2002]公交车路线
题意:有
数据范围:
分析:
这道题是一道图上dp裸题,我们可以定义
核心代码:
inline int abs(int x){
return x<0? -x:x;
}
inline int check(int a,int b){//判断能否到达
return (a==1&&b==8)||(a==8&&b==1)||abs(a-b)==1;
}
tot[1]=1;//从车站A出发
for(t=1;t<=n;t++){
for(i=1;i<=8;i++)
tmp[i]=0;
for(i=1;i<=8;i++)//枚举出发车站
for(j=1;j<=8;j++)//枚举到达车站
if(i!=5&&check(i,j))
tmp[j]=(tmp[j]+tot[i])%mod;
for(i=1;i<=8;i++)
tot[i]=tmp[i];
}
但是这份代码交上去后会T,获得
啊这……
怎么办呢?
我们可以尝试给这份代码换一个形式,再看看有什么特殊的地方:
a[1][2]=a[1][8]=1;//a为可以相互到达点组成的邻接矩阵
a[2][3]=a[2][1]=1;
a[3][2]=a[3][4]=1;
a[4][3]=a[4][5]=1;
//这里没有a[5][4]=a[5][6]=1的原因是到了车站E就不能继续行动了
a[6][5]=a[6][7]=1;
a[7][6]=a[7][8]=1;
a[8][7]=a[8][1]=1;
tot[1]=1;
for(t=1;t<=n;t++){
for(i=1;i<=8;i++)
tmp[i]=0;
for(i=1;i<=8;i++)
for(j=1;j<=8;j++)
if(a[j][i])
tmp[i]=(tmp[i]+tot[j])%mod;
for(i=1;i<=8;i++)
tot[i]=tmp[i];
}
诶诶诶诶诶,好像发现了什么不得了的东西。
让我们再换个形式,让它更加明了:
a[1][2]=a[1][8]=1;
a[2][3]=a[2][1]=1;
a[3][2]=a[3][4]=1;
a[4][3]=a[4][5]=1;
a[6][5]=a[6][7]=1;
a[7][6]=a[7][8]=1;
a[8][7]=a[8][1]=1;
//注意,这里tmp和tot多加了一维
tot[1][1]=1;
for(t=1;t<=n;t++){
for(i=1;i<=1;i++)//这个i的值始终为1,不用管
for(j=1;j<=8;j++)
tmp[i][j]=0;
for(i=1;i<=1;i++)//这个i的值始终为1,不用管
for(j=1;j<=8;j++)
for(k=1;k<=8;k++)
tmp[i][j]=(tmp[i][j]+tot[i][k]*a[k][j]%mod)%mod;//其实就相当于a[k][j]等于1就更新tot
for(i=1;i<=8;i++)
tot[1][i]=tmp[1][i];
}
我们对比一下矩阵乘法的代码:
matrix mul(matrix a,matrix b){
matrix res;
res.len=a.len,res.wid=b.wid;
for(int i=1;i<=res.len;i++)
for(int j=1;j<=res.wid;j++)
res.mt[i][j]=0;
for(int i=1;i<=a.len;i++)
for(int j=1;j<=b.wid;j++)
for(int k=1;k<=a.wid;k++)
res.mt[i][j]=(res.mt[i][j]+a.mt[i][k]*b.mt[k][j]%mod)%mod;
return res;
}
!!!
加上
这可以看为一个变相的Floyd(矩阵加速Floyd会在后面进行讨论):枚举起点
再看看
以此类推,我们就知道这个矩阵的
这刚好与我们的题目不谋而合!
推广到一般情况,给定邻接矩阵留为作业)
现在,我们就可以做一道题目来实践刚刚的结论了!
题意:给定一个带边权的无向图,求从点
数据范围:
分析:
这道题如果去掉边权就是我们上面讨论的结论了,现在考虑怎么加上边权。
可以拆点,将每个点拆做
这样,我们就可以直接上板子了:
#include<stdio.h>
#include<iostream>
using namespace std;
const int maxn=105,mod=2009;
int i,j,k,m,n;
string s;
struct matrix{
int len,wid;
int mt[maxn][maxn];
}st;
inline int getnum(int x,int k){
return (k-1)*n+x;
}
matrix mul(matrix a,matrix b){
matrix res;
res.len=a.len,res.wid=b.wid;
for(int i=1;i<=res.len;i++)
for(int j=1;j<=res.wid;j++)
res.mt[i][j]=0;
for(int i=1;i<=a.len;i++)
for(int j=1;j<=b.wid;j++)
for(int k=1;k<=a.wid;k++)
res.mt[i][j]=(res.mt[i][j]+a.mt[i][k]*b.mt[k][j]%mod)%mod;
return res;
}
int calc(int b){
matrix a=st,res;
res.len=1,res.wid=9*n;
res.mt[1][getnum(1,1)]=1;
for(int i=2;i<=9*n;i++)
res.mt[1][i]=0;
while(b){
if(b&1)
res=mul(res,a);
a=mul(a,a),b>>=1;
}
return res.mt[1][getnum(n,1)];
}
int main(){
scanf("%d%d",&n,&m);
st.len=st.wid=9*n;
for(i=1;i<=n;i++){
cin>>s;
for(j=0;j<n;j++)
if(s[j]!='0')
st.mt[getnum(i,1)][getnum(j+1,s[j]-48)]=1;
}
for(i=1;i<=n;i++)
for(j=1;j<=8;j++)
st.mt[getnum(i,j+1)][getnum(i,j)]=1;
printf("%d\n",calc(m));
return 0;
}
同样,我们再做一道比较新颖的题目来巩固一下:P2151 [SDOI2009]HH去散步
这道题运用了点边互换的小trick,分析详见P2151 [SDOI2009]HH去散步解题报告。
有时候,
题意:给定一张
数据范围:
分析:
这道题比较经典,值得一做。
考虑边的解锁顺序,先套路地对边进行排序(按
考虑怎么添加边:假设已经枚举到了第
那么更新答案也比较简单了:因为我们需要经过
但即使我们使用了矩阵快速幂,我们的复杂度也是
因为我们复杂度的瓶颈在于求
代码:
#include<stdio.h>
#include<bitset>
#include<algorithm>
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=155,maxm=155;
int i,j,k,m,n,t,ans;
int dis[maxn][maxn];
struct matrix{
int len,wid;
bitset<maxn>mt[maxn];
}now,g;
struct edge{
int x,y,d;
}e[maxm];
inline int cmp(edge a,edge b){
return a.d<b.d;
}
matrix mul(matrix a,matrix b){
matrix res;
res.len=a.len,res.wid=b.wid;
for(int i=1;i<=res.len;i++)
for(int j=1;j<=res.wid;j++)
res.mt[i][j]=0;
for(int k=1;k<=a.wid;k++)
for(int i=1;i<=a.len;i++)
if(a.mt[i][k])
res.mt[i]|=b.mt[k];
return res;
}
matrix calc(matrix a,int b,matrix res){
while(b){
if(b&1)
res=mul(res,a);
a=mul(a,a),b>>=1;
}
return res;
}
int main(){
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].d);
sort(e+1,e+1+m,cmp);
ans=inf;
now.len=n,now.wid=n;
for(i=1;i<=n;i++)
now.mt[i][i]=1;
g.len=n,g.wid=n;
for(t=1;t<=m;t++){
now=calc(g,e[t].d-e[t-1].d,now);
g.mt[e[t].x][e[t].y]=1;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++){
if(g.mt[i][j]==0)
dis[i][j]=i==j? 0:inf;
else dis[i][j]=g.mt[i][j];
}
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
for(i=1;i<=n;i++)
if(now.mt[1][i])
ans=min(ans,e[t].d+dis[i][n]);
}
if(ans==inf)
puts("Impossible");
else printf("%d\n",ans);
return 0;
}
有时候,单个的矩阵已经无法满足我们表示状态的需求了,此时我们需要处理多个矩阵,并同样地利用这些矩阵进行加速,我们以P2579 [ZJOI2005]沼泽鳄鱼为例。
题意:给定一个
数据范围:
分析:
如果不考虑食人鱼,这就是一个显然的矩阵快速幂题,但是有了食人鱼的限制,我们就需要把一些点禁掉,不可以到某些点。
但是,食人鱼会动啊?这怎么处理呢?
其实比较简单,我们发现食人鱼的运动周期比较小,只有
有一点需要注意的是,由于矩阵乘法不满足交换律,所以我们在把
代码:
#include<stdio.h>
const int maxn=55,mod=10000;
int i,j,k,l,m,n,s,t,p,fish;
struct matrix{
int len,wid;
int mt[maxn][maxn];
}r[13];
matrix mul(matrix a,matrix b){
matrix res;
res.len=a.len,res.wid=b.wid;
for(int i=1;i<=res.len;i++)
for(int j=1;j<=res.wid;j++)
res.mt[i][j]=0;
for(int i=1;i<=a.len;i++)
for(int j=1;j<=b.wid;j++)
for(int k=1;k<=a.wid;k++)
res.mt[i][j]=(res.mt[i][j]+a.mt[i][k]*b.mt[k][j]%mod)%mod;
return res;
}
int calc(int x,int y,int b){
int rst=b%12;
matrix a,res;
res.len=1,res.wid=n;
for(int i=1;i<=n;i++)
res.mt[1][i]=0;
res.mt[1][x]=1;
if(b<12){
for(int i=2;i<=b+1;i++)
res=mul(res,r[i]);
return res.mt[1][y];
}
a=r[2];
for(int i=3;i<=12;i++)
a=mul(a,r[i]);
a=mul(a,r[1]);
b/=12;
while(b){
if(b&1)
res=mul(res,a);
a=mul(a,a),b>>=1;
}
for(int i=2;i<=rst+1;i++)
res=mul(res,r[i]);
return res.mt[1][y];
}
int main(){
scanf("%d%d%d%d%d",&n,&m,&s,&t,&p);
s++,t++;
for(i=1;i<=12;i++)
r[i].len=n,r[i].wid=n;
for(i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
x++,y++;
for(j=1;j<=12;j++)
r[j].mt[x][y]=r[j].mt[y][x]=1;
}
scanf("%d",&fish);
for(i=1;i<=fish;i++){
int x,y;
scanf("%d",&x);
for(j=1;j<=x;j++){
scanf("%d",&y);
y++;
for(k=j;k<=12;k+=x)
for(l=1;l<=n;l++)
r[k].mt[l][y]=0;
}
}
printf("%d\n",calc(s,t,p));
return 0;
}
上文说过,会对矩阵加速Floyd进行讨论,我们便以此题为例来探讨矩阵乘法重定义后对Floyd的加速。
我们以这道题为例子,来了解如何对矩阵乘法重定义,并证明矩阵加速的正确性。
题意:给定一张
数据范围:
分析:
我们可以先想一想这道题的暴力解法,虽然复杂度不能通过本题,但可以对正解产生一定的启发:
我们可以直接建图,设
(注,文中的Floyd可能枚举顺序与大部分的Floyd顺序不一样,但是仍然是正确的)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
dis[i][j]=i==j? 0:inf;
for(p=1;p<=q;p++){
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
tmp[i][j]=inf;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
for(k=1;k<=n;k++)
tmp[i][j]=min(tmp[i][j],dis[i][k]+g[k][j]);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
dis[i][j]=tmp[i][j];
}
(暴力比正解还难写)
其实,Floyd中核心内容就一句话:
而矩阵乘法的核心是这样:
这两个式子惊人的相似,我们考虑是否可以把矩阵乘法重定义,实现Floyd的转移,同时也要满足结合律(为了让矩阵快速幂可以对其加速)。
我们定义矩阵乘法为:
我们证明一下这个矩阵乘法满足结合律:
设
设
因此,我们证明了这个矩阵乘法满足结合律,这样就可以利用矩阵快速幂对邻接矩阵的Floyd转移进行加速了!
时间复杂度是
代码:
#include<stdio.h>
#include<map>
#define inf 1000000000
using namespace std;
const int maxn=105;
int i,j,k,m,n,s,t,tot;
map<int,int>mp;
struct matrix{
int len,wid;
int mt[maxn][maxn];
}st;
matrix _mul(matrix a,matrix b){
matrix res;
res.len=a.len,res.wid=b.wid;
for(int i=1;i<=res.len;i++)
for(int j=1;j<=res.wid;j++)
res.mt[i][j]=inf;
for(int i=1;i<=a.len;i++)
for(int j=1;j<=b.wid;j++)
for(int k=1;k<=a.wid;k++)
res.mt[i][j]=min(res.mt[i][j],a.mt[i][k]+b.mt[k][j]);
return res;
}
int calc(int x,int y,int b){
matrix a=st,res;
res.len=1,res.wid=n;
for(int i=1;i<=n;i++)
res.mt[1][i]=inf;
res.mt[1][x]=0;
while(b){
if(b&1)
res=_mul(res,a);
a=_mul(a,a),b>>=1;
}
return res.mt[1][y];
}
int main(){
scanf("%d%d%d%d",&k,&m,&s,&t);
for(i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&z,&x,&y);
if(mp.count(x)==0)
mp[x]=++tot;
if(mp.count(y)==0)
mp[y]=++tot;
x=mp[x],y=mp[y];
st.mt[x][y]=st.mt[y][x]=z;
}
s=mp[s],t=mp[t],n=tot;
st.len=n,st.wid=n;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(st.mt[i][j]==0)
st.mt[i][j]=inf;
printf("%d\n",calc(s,t,k));
return 0;
}
在这里,我们来根据P6569 [NOI Online #3 提高组]魔法值谈谈矩阵乘法的倍增预处理与二进制拆分优化。
题意:给定一个
看到这道题目,很显然我们可以重定义一下矩阵乘法:
然后,我们可以让
然后我们每一次询问做一次矩阵快速幂可以做到
但是这个复杂度不能通过本题,需要考虑优化。
我们发现做矩阵快速幂的时候我们的矩阵大小都是
我们预处理所有
但这样的复杂度为什么是对的呢?因为询问的时候进行的矩阵乘法是
这样,时间复杂度就优化到了:
#include<stdio.h>
#include<string.h>
#define int long long
const int maxn=105,maxk=40;
int n,m,q;
int s[maxn];
struct matrix{
int len,wid;
int mt[maxn][maxn];
}zero,ans[maxk];
matrix mul(matrix a,matrix b){
matrix res=zero;
res.len=a.len,res.wid=b.wid;
for(int i=1;i<=a.len;i++)
for(int j=1;j<=b.wid;j++)
for(int k=1;k<=a.wid;k++)
res.mt[i][j]^=(a.mt[i][k]*b.mt[k][j]);
return res;
}
int query(int x){
matrix res;
res.len=1,res.wid=n;
for(int i=1;i<=n;i++)
res.mt[1][i]=s[i];
for(int i=0;i<maxk;i++)
if((x>>i)&1)
res=mul(res,ans[i]);
return res.mt[1][1];
}
signed main(){
zero.len=zero.wid=0;
memset(zero.mt,0,sizeof(zero.mt));
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1;i<=n;i++)
scanf("%lld",&s[i]);
ans[0].len=ans[0].wid=n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans[0].mt[i][j]=0;
for(int i=1;i<=m;i++){
int x,y;
scanf("%lld%lld",&x,&y);
ans[0].mt[x][y]=ans[0].mt[y][x]=1;
}
for(int i=1;i<maxk;i++)
ans[i]=mul(ans[i-1],ans[i-1]);
for(int i=1;i<=q;i++){
int x;
scanf("%lld",&x);
printf("%lld\n",query(x));
}
return 0;
}
学了矩阵加速图上问题,结果同步赛写挂了,只有
这道题是矩阵加速图上问题的一些技巧综合题,前置知识:拆点,预处理多个矩阵进行二进制拆分,即P4159 [SCOI2009] 迷路和P6190 [NOI Online #1 入门组]魔法(民间数据)。
加油,在前面的学习中,你已经锻炼了基本的矩阵乘法思维,现在是实践的时候了!
矩阵加速是一个比较神奇的科技,复杂度是
因此,矩阵加速图上问题的题目大多会有这样的规律:图的点数很小,或者边数很小,同时
再加上矩阵乘法可以有重定义,bitset优化等黑科技,所以矩阵加速图上问题的应用其实非常广(但是我太菜了,并不知道很多应用),希望大家可以探索出更多的毒瘤操作,危害社会(狗头)。
这里给出所有本人已知的此类型题目,有兴趣的读者可以随意切:
参考文献: