把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【洛谷6622】[省选联考 2020 A/B 卷] 信号传递(子集DP)

点此看题面

  • \(m\)个信号站排成一排,相邻两个距离\(1\)单位长度,在最左边的信号站左边\(1\)单位长度有个控制塔。
  • 现在有一个长度为\(n\)的传导路径\(S\),表示第\(i\)次要将信号从编号\(S_i\)的信号站传向编号\(S_{i+1}\)的信号站。
  • 规定从一个信号站向右传每个单位长度只需\(1\)点代价;向左传需要先传到控制塔,再由控制塔传到目标信号站,且传每个单位需\(k\)点代价。
  • 你可以任意决定信号站的编号顺序,求最小的总代价。
  • \(n\le10^5,m\le23\)

贡献转化+子集\(DP\)

发现\(m\)这么小,非常容易想到设\(f_S\)表示从左向右,选择安放了集合\(S\)中的所有信号站时的最小代价。

这里主要是考虑把\(x\)向左传到\(y\)的代价拆成两部分,即从\(x\)\(y\)\(y\)到原点一个来回。

其中从\(x\)\(y\)的部分和向右传递是一样的(只是代价不同而已),我们可以把这两部分的代价一起表示到边上。

即,令\(g_S\)表示一条边左边的点集为\(S\),右边的点集为\(\complement S\)时,经过这条边的代价总和。也就是说\(g_S\)就等于\(\sum_{i=1}^{n-1}[a_i\in S,a_{i+1}\not\in S]+k\sum_{i=1}^{n-1}[a_i\not\in S,a_{i+1}\in S]\)

具体预处理过程中,我们先求出\(p_{u,v}\)表示传导路径中二元组\((u,v)\)的个数。

然后只要任意找到一个\(e\in S\),从\(g_{S\oplus e}\)转移,枚举剩余所有元素,消去\(S\oplus e\)中的元素和\(e\)之间的贡献,加上\(e\)\(\complement S\)中的元素的贡献即可。

还有一部分是\(y\)到原点一个来回的代价,发现这部分与\(x\)的具体位置是没有联系的,可以直接在把\(y\)加入集合的时候计算贡献。

具体地,我们预处理出\(c_{y,S}\)表示集合\(S\)中所有元素出现在\(y\)下一位置的总次数,也就是说\(c_{y,s}=\sum_{i=1}^{n-1}[a_i=y,a_{i+1}\in S]\)

发现\(S\)中的元素和\(y\)计算贡献时是独立的,所以可以任意找到一个\(e\in S\),从\(c_{y,S\oplus e}\)转移,加上\(y\)\(e\)之间的贡献\(c_{y,e}\)即可。

这里要注意一个问题,就是\(m\times2^m\)的数组是开不下的,但实际上我们不需要考虑每个元素在它自己之后的情况,也就是说只需要开一个\(m\times2^{m-1}\)的数组即可,刚好卡在内存上界。

最后的转移就非常简单了,枚举一个不在\(S\)中的元素\(e\),得到:

\[f_{S\cup e}=\min\{f_S+g_S+2(Cnt(S)+1)\times k\times c_{e,\complement S}\} \]

其中\(Cnt(S)\)表示\(S\)\(1\)的个数,而\(Cnt(S)+1\)就是\(e\)的位置,也就是它到控制塔的距离。

代码:\(O(m2^m)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define M 23
#define Gmin(x,y) (x>(y)&&(x=(y)))
using namespace std;
int n,m,k,a[N+5],p[M][M],g[1<<M],_c[M][1<<M-1];long long f[1<<M];
I int& c(CI x,CI y) {return _c[x][(y>>x+1<<x)|(y&((1<<x)-1))];}//从状态中除去第x位,卡内存
namespace FastIO
{
	#define FS 100000
	#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
	char oc,FI[FS],*FA=FI,*FB=FI;
	Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
	Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
}using namespace FastIO;
I int Log(RI x) {RI t=0;W(x>>=1) ++t;return t;}I int Cnt(RI x) {RI t=0;W(x) x^=x&-x,++t;return t;}//Log由状态回到编号;Cnt统计1的个数
int main()
{
	RI i,j,x,l;for(read(n,m,k),l=(1<<m)-1,i=1;i<=n;++i) read(a[i]),--a[i];//方便起见把编号都减1
	for(i=1;i^n;++i) a[i]^a[i+1]&&++p[a[i]][a[i+1]];for(i=2;i<=n;++i) a[i]^a[i-1]&&++c(a[i],1<<a[i-1]);//统计p以及初始的c
	for(i=0;i^m;++i) for(j=0;j<=(l>>1);++j) _c[i][j]=_c[i][j^(j&-j)]+_c[i][j&-j];//预处理c,任选一个元素删去后加上贡献
	for(i=1;i^l;++i) for(x=Log(i&-i),g[i]=g[i^(1<<x)],j=0;j^m;++j)//预处理g,任选一个元素
		x^j&&(i>>j&1?g[i]-=p[j][x]+k*p[x][j]:g[i]+=p[x][j]+k*p[j][x]);//统计把g从S的补集移到S贡献的变化
	for(i=1;i<=l;++i) f[i]=1e18;for(i=0;i^l;++i)//子集DP
		for(x=Cnt(i)+1,j=0;j^m;++j) !(i>>j&1)&&Gmin(f[i^(1<<j)],f[i]+g[i]+2LL*x*k*c(j,l^i));//枚举一个不在集合中的元素转移
	return printf("%lld\n",f[l]),0;
}
posted @ 2021-03-23 16:29  TheLostWeak  阅读(63)  评论(0编辑  收藏  举报