题解 P6622 【[省选联考 2020 A/B 卷] 信号传递】
为表述方便,以下称信号站为点,传输序列中相邻两个点之间连一条从前往后的有向边。
一个显而易见的结论
设边
显然可以将此代价分到各个点上来累加。具体来说,对于最终在坐标为
然而我考场上并没有想到这个显而易见的结论,只拿了30分,我太菜了。
一个基本的做法
容易发现在上述转化下,一个点的贡献只与坐标在其前面的点集有关,只序预处理出
如果暴力计算
但显然可以从小到大枚举
优化
接下来就是大家喜闻乐见的卡空间环节。其实到了这一步基本上就是各凭本事了,做法也不是唯一的。就我所知大概有七种做法,本质不同的有四种。
方法一:强行折半
注意到
于是我们可以分两步走,先求出
这样每时每刻我们只需存储大约一半的
本方法在任何角度都有完全优于它的算法存在,只是作为一个引入。
方法二:滚动数组
原理与方法一相似,但分层更为细致。我们严格按照
那么我们同一时刻最多需要存储的
因此空间开销为
缺点是滚动数组是三维的,寻址十分缓慢。优势在于空间比较小。详见Fuyuki队长的题解。
方法三:巧妙折半
注意到,我们没有必要考虑
空间开销与方法一致,为
#include<iostream>
#include<cstdio>
#define gmi(a,b) a=a<b?a:b
#define FOF(i,a,b) for(int i=a;i< b;i++)
using namespace std;
const int M=23,N=1<<M,INF=1e9;
int n,m,K,x=-1,y,z,w,L,R;
int lg[N],sz[N],f[N],g[M][M],h[M][N>>1];
int main(){
scanf("%d%d%d",&n,&m,&K);L=1<<m;R=L>>1;lg[0]=-1;
while(n--) scanf("%d",&y),~x?++g[x][--y]:--y,x=y;
FOF(i,1,L) lg[i]=lg[i>>1]+1,sz[i]=sz[i>>1]+(i&1);
FOF(i,0,m){
FOF(j,0,m)if(i^j) h[i][0]+=g[j][i]*K-g[i][j];
FOF(j,1,R) y=j&-j,z=lg[y],z+=z>=i,h[i][j]=h[i][j^y]+g[i][z]*(1+K)+g[z][i]*(1-K);
}
FOF(i,1,L)for(f[i]=INF,x=i;y=x&-x;x^=y)
z=lg[y],w=i^y,gmi(f[i],f[i^y]+h[z][w&y-1|w>>z+1<<z]*sz[i]);
cout<<f[L-1]<<'\n';
}
方法四:经典结论
二进制从
0 数到n ,所有比特位变化的总次数为O(n) 。
我们直接去掉
根据上述结论,我们对每个比特位的变化,修改
更强悍的是,这种做法只需要
缺点不是很明显,硬要说的话就是跑得还不够快(洛谷上不吸氧至多
#include<iostream>
#include<cstdio>
#define gmi(a,b) a=a-b>>31?a:b
#define FOF(i,a,b) for(int i=a;i<b;i++)
using namespace std;
const int M=23,N=1<<M;
int n,m,K,x,y,z,w,s,L,R;
int f[N],g[M][M],h[M],c[M][M],lg[N],sz[N];
int main(){
scanf("%d%d%d",&n,&m,&K),x=-1;
while(n--) scanf("%d",&y),~x?++g[x][--y]:--y,x=y;
FOF(i,0,m)FOF(j,0,m)if(i^j)
h[i]+=g[j][i]*K-g[i][j],c[i][j]=g[i][j]*(1+K)+g[j][i]*(1-K);
R=1<<m;L=R-1;lg[0]=-1;
FOF(i,1,R) sz[i]=sz[i>>1]+(i&1),lg[i]=lg[i>>1]+1,f[i]=1e9;
FOF(i,0,L){
s=sz[i]+1;
for(x=L^i ;y=x&-x;x^=y) w=f[i]+h[lg[y]]*s,gmi(f[i|y],w);
for(x=i^i+1;y=x&-x;x^=y){
w=lg[y];
if(y&i) FOF(j,0,m) h[j]-=c[j][w];
else FOF(j,0,m) h[j]+=c[j][w];
}
}
cout<<f[L]<<'\n';
}
方法五:巧妙递推
依旧是沿着方法四的思路,二进制顺序枚举
容易证明,枚举到
这样时空复杂度不变,但常数得到了很大优化,洛谷不吸氧可过。
#include<iostream>
#include<cstdio>
#define gmi(a,b) a=a-b>>31?a:b
#define FOF(i,a,b) for(int i=a;i<b;i++)
using namespace std;
const int M=23,N=1<<M;
int n,m,K,x,y,z,s,L,R;
int f[N],g[M][M],w[M][M],c[M][M],lg[N],sz[N];
int main(){
scanf("%d%d%d",&n,&m,&K),x=-1;
while(n--) scanf("%d",&y),~x?++g[x][--y]:--y,x=y;
FOF(i,0,m)FOF(j,0,m)if(i^j)
w[0][i]+=g[j][i]*K-g[i][j],c[i][j]=g[i][j]*(1+K)+g[j][i]*(1-K);
R=1<<m;L=R-1;lg[0]=-1;
FOF(i,1,R) sz[i]=sz[i>>1]+(i&1),lg[i]=lg[i>>1]+1,f[i]=1e9;
FOF(i,0,L){
z=lg[i&-i];
if(s=sz[i]) FOF(j,0,m) w[s][j]=w[s-1][j]+c[j][z];//只有这里和方法四不同
for(x=L^i;y=x&-x;x^=y) z=f[i]+w[s][lg[y]]*(s+1),gmi(f[i|y],z);
}
cout<<f[L]<<'\n';
}
方法六:登峰造极
注意到,方法四、五的空间可以利用方法二的思想进一步优化,可以将
此时
可能有人觉得出题人卡空间,但殊不知出题人给我们
本方法没有经过实现,但理论分析表明,本算法受滚动数组和
方法七:分块
又是一个很粗暴的思路,既然难以一次性存储
这样
缺点是代码难度(相对方法四、五来说)比较高,优点在于常数非常小,现在luogu和LOJ的最优解都是这种做法(应该是同一个人)。