1 条题解
-
0
自动搬运
来自洛谷,原作者为

BJpers2
**搬运于
2025-08-24 22:22:31,当前版本为作者最后更新于2020-06-27 10:48:51,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
为表述方便,以下称信号站为点,传输序列中相邻两个点之间连一条从前往后的有向边。
一个显而易见的结论
设边的代价为(是坐标)
$$\begin{cases} y-x & x\le y \\ kx+ky & x>y \end{cases} $$显然可以将此代价分到各个点上来累加。具体来说,对于最终在坐标为的点,序列中每有一条的边,若则付出的代价,否则付出的代价。同理对于的边分别付出和的代价。最终总代价等于每个点的代价分别乘其坐标再求和。
然而我考场上并没有想到这个显而易见的结论,只拿了30分,我太菜了。一个基本的做法
容易发现在上述转化下,一个点的贡献只与坐标在其前面的点集有关,只序预处理出表示号点前面的点集为时对答案的贡献,即可用以下转移求出答案:
如果暴力计算(枚举以及中的元素),时间复杂度为。
但显然可以从小到大枚举递推,此时时间复杂度优化为,但空间复杂度也为,如果开的数组,将花费的空间,不可承受。(因为主要的空间瓶颈是数组,为方便计算,以下都只计算的空间开销)
优化
接下来就是大家喜闻乐见的卡空间环节。其实到了这一步基本上就是各凭本事了,做法也不是唯一的。就我所知大概有七种做法,本质不同的有四种。
方法一:强行折半
注意到数组的递推具有比较强的拓扑性,即值会转移到某个大小恰比大的集合。同理。
于是我们可以分两步走,先求出的所有,并求出所有相应的;然后清空数组(但保留恰为的),把空间腾出来存储所有的。
这样每时每刻我们只需存储大约一半的,空间大概是,可以承受。
本方法在任何角度都有完全优于它的算法存在,只是作为一个引入。
方法二:滚动数组
原理与方法一相似,但分层更为细致。我们严格按照单调不降处理所有的和,用滚动数组实现的递推。
那么我们同一时刻最多需要存储的状态个数取决于哪个大小的集合最多,根据常识可知有分别有个集合大小为和,是最多的。
因此空间开销为$2\times 23\times \binom{23}{12}\times 4\text{B}\approx 237\text{M}$。
缺点是滚动数组是三维的,寻址十分缓慢。优势在于空间比较小。详见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'; }方法四:经典结论
二进制从数到,所有比特位变化的总次数为。
我们直接去掉的那一维。然后按二进制数大小(而不是集合大小)顺序枚举,并按照相对的变化暴力修改。
根据上述结论,我们对每个比特位的变化,修改的代价是。那么总修改代价就为,时间复杂度正确。
更强悍的是,这种做法只需要的空间,每多开一个数组只消耗的空间,总空间可以限制在以内。
缺点不是很明显,硬要说的话就是跑得还不够快(洛谷上不吸氧至多分)。优点在于,不但空间优秀,而且实现十分无脑,是本人最喜欢的算法。
#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'; }方法六:登峰造极
注意到,方法四、五的空间可以利用方法二的思想进一步优化,可以将个数组进行滚动。这样一来数组就没有了存在的意义,而数组只会调用的幂,可以不用开那么大的空间。
此时是空间的唯一瓶颈,滚动优化之,空间达到了$2\times \binom{23}{12}\times 4\text{B}\approx 10\text{M}$。
可能有人觉得出题人卡空间,但殊不知出题人给我们已经是最大的宽容。(估计来个就真成经典了
本方法没有经过实现,但理论分析表明,本算法受滚动数组和被阉割的影响,运行效率很可能不如方法四。(当然你用ctz我也没话说)
方法七:分块
又是一个很粗暴的思路,既然难以一次性存储对所有集合的代价,那就把集合拆成前位和后位两部分,即
这样和的空间都得到了开根级别的优化,空间开销相对于来说已经可以忽略不计。其余部分照做即可。
缺点是代码难度(相对方法四、五来说)比较高,优点在于常数非常小,现在luogu和LOJ的最优解都是这种做法(应该是同一个人)。
- 1
信息
- ID
- 5671
- 时间
- 3000ms
- 内存
- 512MiB
- 难度
- 6
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者