专栏文章
0606
个人记录参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @mip4qmzp
- 此快照首次捕获于
- 2025/12/03 06:08 3 个月前
- 此快照最后确认于
- 2025/12/03 06:08 3 个月前
NFLS T2 (AT_arc128_f)
有 张编号为 到 的卡片。第 张卡片上写着整数 。这里, 是偶数。
Snuke 和 Robot 将玩一个游戏,规则如下。
- 游戏主宣布一个排列 ,给 Snuke 和 Robot。
- 然后,Snuke 和 Robot 轮流进行,Snuke 先开始。每个回合的规则如下。
- Snuke 的回合:选择一张剩余卡片并烧掉。
- Robot 的回合:选择具有最大 的卡片 并烧掉。
- 当没有卡片剩余时游戏结束。
游戏的最终得分是 Snuke 吃掉的卡片上整数的总和。Snuke 将以最佳方式玩以最大化得分。
有 个排列 的 。找到所有这些排列的最终得分总和,模 。
- is even.
- All values in input are integers.
NFLS 还要求支持动态修改
题解
好题,学到很多
先不考虑动态修改
为方便令
首先发现我们可以把每个 的贡献拎出来。
具体地,若 在 个排列中的 个里被烧掉了,那么显然它贡献的得分是
同时我们发现这样一来我们不关心 是什么了,我们只关心相对大小关系
于是,如果能求出 表示第 大的数被用掉的排列数量,对 降序排序,答案显然就是 ,这样修改也是好做的。
问题转化成如何求出
值域是 还是太难做了,考虑求出前 大的数被用掉的总次数
这样一来,我们可以把所有 的数看作 ,把所有 的数看作 ,转变成了一个 问题, 就等于对于所有含有 个 且长度为 的 序列做上述游戏,被 Snuke 选中的 的总数。同时我们不关心 内部的具体数值,因此 还要乘上
对式子的推导暂时到头了,现在我们需要来挖掘这个游戏的性质。容易发现前 个数里 Snuke 最多选 个,前 个里最多选 个,以此类推,前 个数里最多选 个。如果把 Snuke 选择的数看作 ,没选的看作 ,那我们需要保证所有前缀和均不小于 。
然而这样还是太抽象了,一个思路是倒着考虑后缀限制。为了第 个选择的数的前缀和合法,它前面必须不选至少 个数。因此第 个数的选择范围是 。
所以我们对于一个确定的 序列有了一个确定的算法来求出会选几个
- 每 个 打包,不妨设每包有 个 ,则一共有 个包,
- 倒着从 到 遍历
- 先累加
- 根据上面的结论,现在我们不得不取一个数。尝试 是否大于 ,如果是就可以取一个 ,否则就得取 了。
枚举所有 现在就可以做到 的时间求出 了,比 好多了。我们尝试进一步优化
追踪 的变化,每次操作相当于 。我们把它搬到平面网格上。
转化成:
-
起始位于
-
每次可以向右上、右、右下三个方向前进(分别对应 等于 )
-
一共走 步
-
向右的方案系数是 (对应 ,此时包内方案有 和 两种)
-
向右上、右下的方案系数是
-
如果走到了 轴下方, 我们强行向上回到 轴,并称这一步 "失败"(对应无法从 中取 的情况)
-
否则,我们称这一步 "成功 "(对应从 中取 的情况)
这样一来,每条路径都对应了一种 的变化序列。这个变化序列所对应的 序列数量就是每步的方案系数乘积,取出 的数量就是 "成功" 的步数。
这一步转化其实很有意思。
- 枚举的是所有 序列
- 根据每个 序列来计算一系列 的变化
- 根据 的变化来求出我们取了多少
我们需要的只是最后的答案,而 序列和 的变化并非一一对应,而是多对一的关系。这一步转化使得我们不必再枚举 序列,转而考虑更好刻画的 变化问题——我们把后者成功放到了网格图上考虑,更直观了
现在要继续进一步优化。这个低于 轴拉回的限制太抽象了,考虑直接不拉回会怎么样。容易发现,被拉回的步数等价于不拉回时的最小值。于是变成:
-
起始位于
-
每次可以向右上、右、右下三个方向前进(分别对应 等于 )
-
一共走 步
-
终点必须是 (因为 ,所以向上总贡献是 ,向下总贡献是 ,落点在 )
-
向右的方案系数是 (对应 ,此时包内方案有 和 两种)
-
向右上、右下的方案系数是
-
对应的 序列数量就是每步的方案系数乘积
-
设走过的最小值为 ,则取出 的数量就是
-
这个路径对 的贡献是
如果在给定 的情况下,我们能快速计算所有路径的方案系数乘积之和,我们就做完了。不妨设其为
钦定最小值是 不好考虑,我们尝试对于最小值 的路径求出答案后差分,设其为
为了解决掉这个最小值 的限制,可以利用折线法做一个小容斥。
设不限制最小值时的答案为 ,即从 按刚才的方法走到 的所有路径的方案系数乘积之和。我们需要从这里面去掉最小值 的路径的答案。
对于每个最小值 的路径,它一定会经过 这条直线。找到它第一次访问 的点,称其为 ,我们沿 对陈路径在 后的部分,终点变为 。我们发现,所有从 到新终点的路径都对应一条原问题的非法路径。证明可以直接考虑翻转回去。
于是
问题最后变成怎么快速计算 。事实上我们要计算的是
很巧妙的一个式子,每步的选择变为了多项式每项的选择,纵坐标转化成了 的幂次,方案系数转化为了 的系数。
我趣,二项式定理
那很爽了,我们可以计算
注意 的取值范围。 显然, 是因为 需要 ,因为一共只有 个 ,不能选更多了。
后面推式子部分是简单的。注意到 内是一个相减的形式,刚好可以随着 增加两两抵消,最后做个前缀和就可以了。
修改的部分也是简单的。因为修改的差值很小,考虑每次给 怎么做,发现只有 个变化,暴力做就可以了。总复杂度
CPPint fac[5000001];
int inv[5000001];
int C(int n,int m){
if(n<m) return 0;
return mul(fac[n],inv[m],inv[n-m]);
}
int g[MAXN];
int f[MAXN];
int G(int n,int k){
return C(2*n,k);
}
int F(int n,int k,int L){
return rmv(G(n,k),G(n,2*(L-1)-(k-n)+n));
}
int S(int n,int k,int L){
return rmv(F(n,k,L),F(n,k,L+1));
}
int n,q;
int a[MAXN];
void gen(){
static int c[5000005];
static int s[5000005];
foru(i,0,2*n){
c[i]=C(2*n,i);
}
s[0]=c[0],s[1]=c[1];
foru(i,2,5*n){
s[i]=add(s[i-2],c[i]);
}
auto qry=[](int l,int n)->int {
int ret=s[l+2*(n-1)];
if(l>=2) mmv(ret,s[l-2]);
return ret;
};
foru(k,1,n){
mdd(g[k],mul(n,C(2*n,2*n-k)));
mmv(g[k],mul(n,C(2*n,2*n+k+2)));
mmv(g[k],mul(n-k,C(2*n,2*(n-k)+k)));
mdd(g[k],mul(n+1,C(2*n,2*(n+1)+k)));
mmv(g[k],qry(k+2*(n-k+1),(n+1)-(n-k+1)+1));
// foru(L,n-k+1,n+1){
// mmv(g[k],c[2*L+k]);
// }
mll(g[k],fac[k],fac[2*n-k]);
}
foru(k,n+1,2*n){
mdd(g[k],mul(n,C(2*n,k)));
mmv(g[k],mul(n,C(2*n,2*n+k+2)));
mmv(g[k],mul(0,C(2*n,2*(0)+k)));
mdd(g[k],mul(n+1,C(2*n,2*(n+1)+k)));
mmv(g[k],qry(2+k,n));
// foru(L,1,n+1){
// mmv(g[k],c[2*L+k]);
// }
mll(g[k],fac[k],fac[2*n-k]);
}
foru(i,1,2*n){
f[i]=rmv(g[i],g[i-1]);
}
}
void solve(bool SPE){
n=RIN/2,q=RIN;
foru(i,1,2*n){
a[i]=RIN;
}
fac[0]=1;
foru(i,1,5000000){
fac[i]=mul(fac[i-1],i);
}
inv[5000000]=qpow(fac[5000000],mod-2);
ford(i,5000000-1,0){
inv[i]=mul(inv[i+1],i+1);
}
gen();
int ans=0;
static int b[MAXN];
foru(i,1,2*n){
b[i]=a[i];
}
sort(b+1,b+1+2*n,[](int x,int y)->bool {
return x>y;
});
foru(i,1,2*n){
// cerr<<b[i]<<end
mdd(ans,mul(f[i],b[i]));
}
static pair<int,int> rg[1000005];
foru(i,1,1000000){
rg[i]={INT_MAX,INT_MIN};
}
foru(l,1,2*n){
int r=l;
while(r+1<=2*n && b[r+1]==b[l]){
r++;
}
// cerr<<b[l]<<' '<<l<<' '<<r<<endl;
rg[b[l]]={l,r};
l=r;
}
auto PUSH=[&ans](int x,int rk)->void {
assert(rg[x].fi==INT_MAX || rg[x].fi-1==rk || rg[x].se+1==rk);
mdd(ans,mul(f[rk],x));
chkmax(rg[x].se,rk);
chkmin(rg[x].fi,rk);
};
auto POP=[&ans](int x,bool L)->void {
assert(rg[x].fi!=INT_MAX);
if(L){
mmv(ans,mul(f[rg[x].fi],x));
rg[x].fi++;
}else{
mmv(ans,mul(f[rg[x].se],x));
rg[x].se--;
}
if(rg[x].fi>rg[x].se) rg[x]={INT_MAX,INT_MIN};
};
auto ADD=[&](int x)->void {
// cerr<<"ADD "<<rg[x].fi<<' '<<rg[x].se<<endl;
assert(rg[x+1].se==INT_MIN || rg[x+1].se+1==rg[x].fi);
assert(rg[x].fi!=INT_MAX);
PUSH(x+1,rg[x].fi);
POP(x,true);
};
auto RMV=[&](int x)->void {
// cerr<<"RMV "<<rg[x].fi<<' '<<rg[x].se<<endl;
PUSH(x-1,rg[x].se);
POP(x,false);
};
auto out=[]()->void {
cerr<<"OP"<<endl;
foru(i,1,1000000){
if(rg[i].fi==INT_MAX) continue;
cerr<<i<<' '<<rg[i].fi<<' '<<rg[i].se<<endl;
}
};
// out();
while(q--){
int x=RIN,y=RIN;
if(y>0){
foru(i,a[x],a[x]+y-1){
// cerr<<i<<endl;
// assert(1<=i && i<=999999);
ADD(i);
}
}
if(y<0){
ford(i,a[x],a[x]+y+1){
// cerr<<i<<endl;
// assert(2<=i && i<=1000000);
RMV(i);
}
// exit(1);
}
a[x]+=y;
printf("%d\n",ans);
// out();
}
return ;
}
这题有几个关键的步骤
- 提取出 ,发现只需要求出 ,同时在这之后的修改也是简单的。
- 针对第 大求 困难,转化成最前 大求 ,此时可以把序列变成 ,只关心一个数是否属于前 大,是从 变为 的关键一步。
- 实际上如果不追求进一步优化,对 也可以用类似的思路? 变为 ,所有比它大的变为 ,比他小的是 ,也可以减小枚举量。但进一步优化会很抽象,不如 序列更好考虑了。
- 在 序列上运行算法,发现 序列与最为关键的 的变化不是一一对应关系,而是多对一的关系。枚举量进一步缩小到只枚举到 的变化路径
- 对困难的 做转化,变为了更好考虑的 最小值
- 差分,需要计算最小值为 的答案,转为计算最小值 的答案
- 折线法进一步转化
- 处理组合数
比赛的时候死在第二步了😰️练过这种题以后应该就好一些了
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...