【题解】[省选联考 2021 A/B 卷] 滚榜

题目大意

已知序列 \(A\) 以及序列 \(B\) 的和 \(m\) 要求对每一个 \(a_i\) 分配一个 \(b_i\) 使得每次分配完之后 \(a_i+b_i\) 都是 \(A\) 序列中所有数字的最大值。

每次修改完 \(a_i\to a_i+b_i\).

Solution:

看到 \(n\) 的范围就会想到状压:设 \(f[S,i,j,k]\) 表示已经揭榜的人的集合为 \(S,\) 上一个揭榜的人是 \(i,\) 他的 \(b\)\(j\) 还剩下 \(k\) 的题目数能用。复杂度是 \(O(2^{n}nm^2)\)

那么看到后面那两个东西很不顺眼……和 \(m\) 有关的维度必须被砍掉。

观察到题面的一条重要信息: \(b_i\)是单调不降的。

那么这个东西怎么用呢?先暂且不谈,考虑一下我们维护 \(b\) 的实际意义有多大。

我们维护它完全是因为后面的暴力转移需要它,它和我们最后的答案 一点关系都没有

那么,我们考虑一下有多少方案是一样的:对于一个固定下来的排名,分配 \(b\) 的方式有很多,但是这种方案只有一个。

从这里就能看出来维护 \(b\) 的冗余性质了。考虑对于一个已知的排名序列 \(p,\) \(b\) 的最优分配(即做到最小)就是每次尽量让 \(b\) 相等。

那么每一个 \(b\) 的作用无非就是让 \(p_{i+1}+b_{i+1}\) 与前一项尽量接近,以此来保证我们选择的 \(\sum b\) 最小。

剩下的和 \(m\) 的差距都可以在最后一项补回来。

但是值得注意的是,我们这样看似漏下很多东西,但实际上,这样分配\(b\) 所丢失的仅仅只是同一个排列下不同的 \(b\) 的排列方案,而这些方案数并不是我们所需要的。

于是,我们可以思考题干中给定那个 \(b\) 不降的条件有啥用了。它也恰好规约了我们在揭榜一个人以后,后面所有人揭榜的增加量都是在 \(b_i\) 以上的。

考虑对 \(b\) 进行差分,就是考虑我们在 \(a_i\) 的时候对它加上了多少。这样,每一个差分后的值 \(c_i\) 都会在后面的揭榜过程中对 \(\sum b\) 贡献 \((n-i+1)c_i\) 的值。

这样我们就会发现,实际上我们已经不需要维护 \(b_j\) 了。只要知道当前状态下最后来的人和 pre-state 最后进来的人,就可以根据他们的 \(a_i\) 推算出最小代价了。因为我们只需要知道最小代价。

至于为什么只需要最小代价……因为我们对每一种排名序列只需要求出一个 \(\sum b\) 最小的就可以了。

于是可以预处理 \(cost[i][j]\) 表示将 \(a_i\) 变成 \(a_j\) 的最小代价。

注意一下题目中给的编号的细节。

那么,现在的方程就可以被我们转化为: \(f[S][i][j]\) 表示揭榜的人状态为 \(S,\) 最后一个进来的人是 \(i,\) 一共用掉了 \(j\) 题的方案数。

于是我们可以枚举状态,枚举 \(i,pre\) 从而转移。转移复杂度 \(O(2^n n^2 m)\)

注意用一些技巧优化,比如 lowbit

回过头来想一下,这个题去掉 \(b\) 的限制,我们实际上用了什么样的思想?

想到了差分,而这一步骤实际上是把每一个人的贡献给分开了——分成了对于全局的贡献。

分开之后呢?我们发现,可以通过对代价的提前计算来求解这个题。

于是,我们就可以把 \(b\) 这一维度放到 代价提前计算 这一部分中了。

所以有一点启示:有时候可以考虑把不想要的状态提前计算掉。

当然需要一些转化。

最后提一下初始化的问题:每次滚榜之后的最大值一定是大于最初始序列的最大值的,而这个最大值也是原序列唯一一个有资格当榜上最大值的数,也就是说,它的所有榜一值上最小的。

所以我们需要计算一个状态的最小值能不能满足条件,这同时也是最开始状态转移——转移到最初榜首的位置,这样才是最小的。这是最小代价,注意。

考场时候没做出来 现在终于明白了)

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,a[200];
int f[1<<14][14][501];
int cost[14][14],maxn[14];
int num[1<<14],pw[1<<14];
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
inline int lowbit(int x){return x&(-x);}
inline int pop_count(int x){
	int res=0;
	for(int j=x;j;j-=lowbit(j))res++;
	return res;
}
void solve(){
	scanf("%lld%lld",&n,&m);
	for(int i=0;i<=13;++i)pw[1<<i]=i;
	for(int i=0;i<n;++i)scanf("%lld",&a[i]);
	for(int i=0;i<n;++i)
		for(int j=0;j<n;++j){
			//min cost{i\to j}
			cost[i][j]=Max(a[j]-a[i]+(i>j),0);
			maxn[i]=Max(maxn[i],cost[i][j]);
		}
	for(int i=0;i<(1<<n);++i)num[i]=pop_count(i);
	for(int i=0;i<n;++i)if(m>=n*maxn[i])f[1<<i][i][n*maxn[i]]=1;//mean that 'i' can be any number
	//最优策略下的数字一定最后不高于变成最大值的代价,所以初始化只能处理掉变成最大值所消耗的代价 
	for(int i=1;i<(1<<n);++i){
		if(num[i]<=1)continue;
		for(int j=i;j;j-=lowbit(j)){
			int state=i-lowbit(j);//state/j
			int pre=pw[lowbit(j)];//pos_j
			for(int k=state;k;k-=lowbit(k)){
				int pstate=lowbit(k);//1<<p
				int pos=pw[pstate];
				if(pos==pre)continue;
				int g=cost[pre][pos]*(n-num[state]);
				for(int v=g;v<=m;++v){
					f[i][pre][v]+=f[state][pos][v-g];
				}
			}
		}
	}
	int ans=0;
	for(int i=0;i<n;++i)
		for(int j=0;j<=m;++j)
			ans+=f[(1<<n)-1][i][j];
	cout<<ans<<endl;
}
signed main(){
	freopen("111.txt","r",stdin);
	solve();
	return 0;
}
posted @ 2021-07-13 10:14  Refined_heart  阅读(149)  评论(0编辑  收藏  举报