[省选联考 2020 A/B 卷] 信号传递(状压dp + 卡空间)

problem

luogu-P6622

一条道路上从左至右排列着 m m m 个信号站,初始时从左至右依次编号为 1 , 2 , … , m 1,2,\dots,m 1,2,,m,相邻信号站之间相隔 1 1 1 单位长度。

每个信号站只能往它右侧的任意信号站传输信号(称为普通传递),每单位长度距离需要消耗 1 1 1 单位时间。

道路的最左侧有一个控制塔,它在最左侧信号站的左侧,与其相隔 1 1 1 单位长度。

控制塔能与任意信号站进行双向信号传递(称为特殊传递),但每单位长度距离需要消耗 k k k 个单位时间。

对于给定的长度为 n n n 的信号传递序列 S S S,传递规则如下:

  1. n − 1 n-1 n1 次信号传递,第 i i i 次信号传递将把信号从 S i S_i Si 号信号站传递给 S i + 1 S_{i+1} Si+1 号。
  2. S i + 1 S_{i+1} Si+1 号信号站在 S i S_i Si 号右侧,则将使用普通传递方式,从 S i S_i Si 号直接传递给 S i + 1 S_{i+1} Si+1 号。
  3. S i + 1 S_{i+1} Si+1号信号站在 S i S_i Si 号左侧,则将使用特殊传递方式,信号将从 S i S_i Si 号传递给控制塔,再由控制塔传递给 S i + 1 S_{i+1} Si+1 号。
  4. S i = S i + 1 S_i=S_{i+1} Si=Si+1,则信号无须传递。

阿基作为大工程师,他能够任意多次交换任意两个信号站的位置,即他能够重排信号站的顺序,这样会使得 S S S 消耗的传递时间改变。

现在阿基想知道,在他重排信号站顺序后, S S S 所消耗的传递时间最小能是多少。

solution

假设坐标在 x , y x,y x,y 的两个信号塔之间有一次 x → y x\rightarrow y xy 的传递,可以形式化地表示:
{ y − x x ≤ y k x + k y x > y \begin{cases} y-x&&x\le y\\ kx+ky&&x>y \end{cases} {yxkx+kyxyx>y
这是同构的,也就是说对于每个传递的贡献可以拆成两个信号塔的各自贡献。

假设坐标在 y y y 的信号塔,如果有坐标 x → y x\rightarrow y xy 的传递。

  • 则对于 y y y 所代表的信号塔而言:
    • x ≤ y x\le y xy,产生 1 1 1
    • x > y x>y x>y,产生 k k k
  • 反之同理,对于 x x x 所代表的信号塔而言:
    • x ≤ y x\le y xy,产生 − 1 -1 1
    • x > y x>y x>y,产生 k k k

则最终代价等于每个点的贡献乘以其坐标再求和。

具体的数学形式推导:

c n t ( i , j ) : cnt(i,j): cnt(i,j): 有多少次信号塔 i → j i\rightarrow j ij 的传递,即 c n t ( i , j ) = ∑ k = 1 n [ S k = i ] [ S k + 1 = j ] cnt(i,j)=\sum_{k=1}^n[S_k=i][S_{k+1}=j] cnt(i,j)=k=1n[Sk=i][Sk+1=j]

设下标 p p p 处是 i d p id_p idp 号信号站,推导一下每个下标对答案的贡献。
a n s = ∑ i = 1 m ∑ j = i + 1 m c n t ( i d i , i d j ) ∗ ( j − i ) + ∑ i = 1 m ∑ j = 1 i − 1 c n t ( i d i , i d j ) ∗ ( i + j ) ∗ k ans=\sum_{i=1}^m\sum_{j=i+1}^mcnt(id_i,id_j)*(j-i)+\sum_{i=1}^m\sum_{j=1}^{i-1}cnt(id_i,id_j)*(i+j)*k ans=i=1mj=i+1mcnt(idi,idj)(ji)+i=1mj=1i1cnt(idi,idj)(i+j)k = ∑ i = 1 m i ( ∑ j = i + 1 m ( k ∗ c n t ( i d j , i d i ) − c n t ( i d i , i d j ) ) + ∑ j = 1 i − 1 ( k ∗ c n t ( i d i , i d j ) + c n t ( i d j , i d i ) ) ) =\sum_{i=1}^mi\bigg(\sum_{j=i+1}^m\Big(k*cnt(id_j,id_i)-cnt(id_i,id_j)\Big)+\sum_{j=1}^{i-1}\Big(k*cnt(id_i,id_j)+cnt(id_j,id_i)\Big)\bigg) =i=1mi(j=i+1m(kcnt(idj,idi)cnt(idi,idj))+j=1i1(kcnt(idi,idj)+cnt(idj,idi)))

f ( s ) : f(s): f(s): 考虑到 ∣ s ∣ |s| s 位( ∣ s ∣ : s |s|:s s:s 二进制下 1 1 1 的个数),前 ∣ s ∣ |s| s 位的信号站编号集合为 s s s 时的最小代价。

那么 ∑ j = i + 1 m ( k ∗ c n t ( i d j , i d i ) − c n t ( i d i , i d j ) ) + ∑ j = 1 i − 1 ( k ∗ c n t ( i d i , i d j ) + c n t ( i d j , i d i ) ) \sum_{j=i+1}^m\Big(k*cnt(id_j,id_i)-cnt(id_i,id_j)\Big)+\sum_{j=1}^{i-1}\Big(k*cnt(id_i,id_j)+cnt(id_j,id_i)\Big) j=i+1m(kcnt(idj,idi)cnt(idi,idj))+j=1i1(kcnt(idi,idj)+cnt(idj,idi)) 形式中的 j j j 其实就是以 ∣ s ∣ |s| s 为划分点,分成在 s s s 集合内的编号信号塔和不在的信号塔。

于是我们可以预处理这部分的贡献,只需要知道 i i i 和集合 s s s 即可。

规范化的,令 g ( i , s ) = ∑ j ∉ s ∧ j ≠ i k ∗ c n t ( i d j , i d i ) − c n t ( i d i , i d j ) + ∑ j ∈ s k ∗ c n t ( i d i , i d j ) + c n t ( i d j , i d i ) g(i,s)=\sum_{j\not\in s\wedge j\ne i}k*cnt(id_j,id_i)-cnt(id_i,id_j)+\sum_{j\in s}k*cnt(id_i,id_j)+cnt(id_j,id_i) g(i,s)=jsj=ikcnt(idj,idi)cnt(idi,idj)+jskcnt(idi,idj)+cnt(idj,idi)

先预处理 g ( i , 0 ) g(i,0) g(i,0),然后每次枚举 s s s 二进制下的一个 1 1 1(随便拎个 l o w b i t lowbit lowbit 位)即可 O ( 1 ) O(1) O(1) 转移。

一个数从 ∉ s \not\in s s 到变成 ∈ s \in s s,先减去原来的代价再加上 ∈ s \in s s 里面时计算的代价。

时间复杂度 O ( 2 m m ) O(2^mm) O(2mm)

那么 f f f 数组的转移就比较简洁了:枚举 s s s 二进制下的 1 1 1 位置,取较小值。
f ( s ) = min ⁡ i ∈ s { f ( s − { i } ) + ∣ s ∣ g ( i , s − { i } ) } f(s)=\min_{i\in s}\Big\{f(s-\{i\})+|s|g(i,s-\{i\})\Big\} f(s)=ismin{f(s{i})+sg(i,s{i})}
到此时空复杂度均为 O ( m 2 m ) O(m2^m) O(m2m)

发现空间起飞了。其实观察数据分布就会发现, m m m 的变化只有 1 1 1,对空间的限制明显狠于时间。

也就是说,难得的需要来考虑卡一下空间了。

有各种大力优化空间的妙做法,但是从应试角度,以及出题角度,私以为这种做法以及可以应付了:

转移方程中 g ( i , s ) g(i,s) g(i,s) ,若 i ∈ s i\in s is 则是不合法的,显然不会被用到。

换言之,对于每个 i i i,最多只有 2 22 2^{22} 222 种状态。

如果只存合法状态,内存就会 / 2 /2 /2,在时间上而言是常数的东西这里就是个大优化了。

考虑怎么减半?

  • 只用让每个 s s s 的前 i − 1 i-1 i1 位不动,(s&(1<<i)-1)
  • 其余位置整体右移一位,相当于 / 2 /2 /2(s^(s&(1<<i)-1))>>1
  • 最后把两者加起来即可。

code

#include <bits/stdc++.h>
using namespace std;
#define maxn 23
int n, m, k;
int f[1 << maxn], g[maxn][1 << maxn - 1], cnt[maxn][maxn];
int lowbit( int x ) { return x & -x; }
int main() {
	scanf( "%d %d %d", &n, &m, &k );
	for( int i = 1, x, lst = -1;i <= n;i ++ ) {
		scanf( "%d", &x ); x --;
		if( ~ lst ) ++ cnt[lst][x];
		lst = x;
	}
	for( int i = 0;i < m;i ++ ) {
		for( int j = 0;j < m;j ++ )
			if( i ^ j ) g[i][0] += k * cnt[j][i] - cnt[i][j];
		for( int s = 1;s < (1 << m);s ++ )
			if( ! (s >> i & 1) ) {
				int t = s ^ lowbit( s );
				int j = __builtin_ffs( s ) - 1;
				g[i][(s & (1<<i)-1) + ((s ^ (s & (1<<i)-1)) >> 1)] = 
				g[i][(t & (1<<i)-1) + ((t ^ (t & (1<<i)-1)) >> 1)] + 
				(k * cnt[i][j] + cnt[j][i]) - (k * cnt[j][i] - cnt[i][j]);
			}
	}
	memset( f, 0x3f, sizeof( f ) ); f[0] = 0;
	for( int s = 1;s < (1 << m);s ++ ) {
		int x = __builtin_popcount( s );
		for( int i = 0;i < m;i ++ )
			if( s >> i & 1 ) {
				int t = s ^ (1 << i);
				f[s] = min( f[s], f[t] + x * g[i][(t & (1<<i)-1) + ((t ^ (t & (1<<i)-1)) >> 1)] );
			}
	}
	printf( "%d\n", f[(1 << m) - 1] );
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值