省选训练好题
好久没更了,发现最近做的有意思题还蛮多的,但是自己最近的状态太不如人意了,要找找状态。
XX Open Cup, Grand Prix of Tokyo, B: Evacuation
考虑这样一件事,就是这个区间询问其实有点假,你只需要考虑你先碰到的那条左边界/右边界就行了,所以说求出中线之后这实际上就是一个前缀/后缀询问。
设 \(f(x,r)\) 表示右边界在 \(r\) 的时候如果旅游团在 \(x\) 疏散需要的最小代价。那么我们相当于要求 \(\max_{i\geq mid} f(i,r)\)。
考察 \(f(x,r)\) 函数的一些性质,我们首先考察 \(f(x,r+1)-f(x,r)\) 即右移右边界贡献是怎么个变法,你发现除了已经在避难所的那些人,其它所有的人都需要多花一的代价。
随着 \(r\) 的右移,已经在避难所的人一定越来越多,所以 \(f(x,r)\) 关于 \(r\) 是凸的。
三分但是你是固定 \(r\) 求 \(x\) 的最大值,显然还有说法。你再考察对于相邻的 \(x,x+1\),在 \(r\) 处的“边际效应”,也就是说 \(f(x,r+1)-f(x,r)\) 与 \(f(x+1,r+1)-f(x+1,r)\)。由于你一开始越靠右,已经在避难所里的人就会越少,贡献就会加得快。边际效应递增换句话说就是四边形不等式:\(f(x,r+1)-f(x,r)\leq f(x+1,r+1)-f(x+1,r)\)。
这告诉我们这道题拥有决策单调性。即对于 \(f\) 矩阵的每一个局部,找出对于每一个 \(r\),\(\mathrm{argmax}_x f(x,r)\),那么这个 \(\mathrm{argmax}\) 是递增的。(否则显然违反四边形不等式)
我们现在有一个做法了:直接把询问挂上线段树然后跑决策单调性分治。复杂度 \(O(n\log^2 n)\),如果你说排序可以不带 \(\log\) 可以做到单 \(\log\)。
有更优美的做法,注意到对于两个 \(x<y\) 关于 \(r\) 的函数 \(f(x,r)/f(y,r)\) 至多只有一个交点:前面 \(f(x,r)>f(y,r)\) 后面反过来。
于是你扫描线处理每一个后缀,具体地:
每一次维护对于每一个 \(r\) 是哪一个 \(x\) 取到了最大值,每一次加入一个 \(x\) 的所有函数值 \(f(x,r)\) 并更新每个位置的最大值:发现它称为最大值的位置一定是当前后缀的一段前缀,于是我们拿栈存一下所有的区间就可以了。
询问的话直接在这个结构上二分。
#include <cstdio>
#include <vector>
#include <random>
#include <cassert>
#include <algorithm>
#include <functional>
using namespace std;
template<typename T=int>
T read(){
char c=getchar();T x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=200003;
typedef long long ll;
int n,q;
ll sum,a[N];
int len[N];
ll s[N],ss[N],res[N];
vector<int> vec[N];
int ql[N],qr[N],qm[N];
inline ll w(int l,int r){return (ss[r]-ss[l])-l*(s[r]-s[l]);}
inline ll f(int x,int r){
if(len[x]<0) return 0;
int t=min({r-x,len[x],x-1});
return w(x,x-t-1)+w(x,x+t)+(sum-s[x+t]+s[x-t-1])*(t+1);
}
int stk[N],pos[N],tp;
void solve(){
for(int i=1;i<=n;++i){
s[i]=s[i-1]+a[i];
ss[i]=ss[i-1]+(ll)i*a[i];
}
for(int i=1;i<=q;++i){
if(qm[i]>qr[i]) continue;
vec[qm[i]].emplace_back(i);
}
tp=0;
for(int i=n;i;--i){
while(tp&&f(i,pos[tp])>=f(stk[tp],pos[tp])) --tp;
if(tp){
int l=stk[tp],r=pos[tp];
while(l<r){
int mid=(l+r)>>1;
if(f(i,mid)>=f(stk[tp],mid)) l=mid+1;
else r=mid;
}
pos[++tp]=l-1;
}
else pos[++tp]=n;
stk[tp]=i;
for(int x:vec[i]){
int t=upper_bound(pos+1,pos+tp+1,qr[x],greater<int>())-pos-1;
res[x]=max(res[x],f(stk[t],qr[x]));
}
vec[i].clear();
}
}
int main(){
n=read();sum=read<ll>();
for(int i=1;i<=n;++i) a[i]=read<ll>(),s[i]=s[i-1]+a[i];
for(int i=1;i<=n;++i){
int l=0,r=min(i,n-i+1);
while(l<r){
int pos=(l+r)>>1;
if(s[i+pos]-s[i-pos-1]>sum) r=pos;
else l=pos+1;
}
len[i]=l-1;
}
q=read();
for(int i=1;i<=q;++i){ql[i]=read();qr[i]=read();qm[i]=(ql[i]+qr[i]+1)>>1;}
solve();
reverse(a+1,a+n+1);
reverse(len+1,len+n+1);
for(int i=1;i<=q;++i){
ql[i]=n-ql[i]+1;
qr[i]=n-qr[i]+1;
swap(ql[i],qr[i]);
qm[i]=n-qm[i]+2;
}
solve();
for(int i=1;i<=q;++i) printf("%lld\n",res[i]);
return 0;
}
CEOI 2020 象棋世界
大家 K 的 Case 其实有点推麻烦了,其实最难的是 B?
-
P:判一下是不是 \(c_1=c_r\) 就行了。否则无解。
-
R:同上。否则需要走两步,有两种走法。
-
Q:注意到答案不超过 2 所以只有一些分讨,但比想象的情况多,比如你需要考虑 \(n=m\) 的时候,Q 可以先走到某一个角落然后斜着一步走到。一种比较好的方法是直接做 \(O(1)\) 条一次函数暴力分讨交点。
然后对于 B 来说,为了保证你的跳跃次数尽可能少,你每一次一定跳得尽量极端,除非你下一步可以直达终点。
所以先模拟求出最短路径形态:我们枚举第一次跳跃的方向,然后不断跳极端,找到在你这条路径上第一个 \(x\geq n,y=c_R\) 的位置作为暂定终点。
然后考虑再在上面调整,每一次可以让一个拐角少走一步,终点向下平移两步。那么这就是一个插板的组合数。
对于 K 来说,由于 \(R\geq C\),然后你每次最多往上一行,所以你一定恰好走 \(R-1\) 步。
于是你需要计算一个“两条线问题”:从 \((1,c_1)\) 游走到 \((R,c_R)\),不能触碰 \(x=0\) 和 \(x=C+1\) 两条线的方案数。
这是这道题的一维情况。这很经典,你施加反射容斥,然后你只需要考虑计算到达横坐标 \(\equiv \pm c_R \pmod {2R+2}\) 的点的方案数。
这相当于计算 \((x+\frac{1}{x}+1)^{R-1}\) 在 \(\bmod (x^{2R+2}-1)\) 循环卷积意义下的结果。这道题没有 NTT 模数所以你不得不暴力卷积得到 \(O(n^2 \log n)\) 的复杂度。才不写 MTT 呢
#include <cstdio>
#include <cctype>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(!isdigit(c)) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(isdigit(c));
return x;
}
const int N=1003,P=1000000007;
int n,m,q,lim;
typedef long long ll;
int qp(int a,int b=P-2){
int res=1;
while(b){
if(b&1) res=(ll)res*a%P;
a=(ll)a*a%P;b>>=1;
}
return res;
}
struct Poly{
int f[N<<1];
friend Poly operator*(const Poly x,const Poly y){
Poly z;
for(int i=0;i<lim;++i) z.f[i]=0;
for(int i=0;i<lim;++i)
for(int j=0;j<lim;++j){
int t=i+j;
if(t>=lim) t-=lim;
z.f[t]=(z.f[t]+(ll)x.f[i]*y.f[j])%P;
}
return z;
}
}F,G;
int fac[N],fiv[N];
int C(int a,int b){
int res=fiv[b];
for(int i=0;i<b;++i) res=(ll)res*(a-i)%P;
return res;
}
void solve(){
char c=getchar();
while(!isupper(c)) c=getchar();
int x=read(),y=read();
if(c=='P'){
if(x!=y) puts("0 0");
else printf("%d 1\n",n-1);
return;
}
if(c=='R'){
if(x==y) puts("1 1");
else puts("2 2");
return;
}
if(c=='Q'){
if(n==m&&((x==1&&y==m)||(x==m&&y==1))) puts("1 1");
else if(x==y) puts("1 1");
else{
int res=4;
if((x^y^n)&1){
if(x+y+n-1<=m+m) ++res;
if(x+y-n+1>=2) ++res;
}
if(n==m){
if(x==1||x==m) ++res;
if(y==1||y==m) ++res;
}
printf("2 %d\n",res);
}
return;
}
if(c=='K'){
int a=(y-x+lim)%lim;
int b=(-x-y+lim)%lim;
int res=F.f[a]-F.f[b];
if(res<0) res+=P;
printf("%d %d\n",n-1,res);
return;
}
if(c=='B'){
if((x^y^n)&1){
int p,ps,gap=2*(m-1);
int tt,res=0x3f3f3f3f,cnt=0;
for(int op=0;op<2;++op){
if(op){ps=1;p=x;}
else{ps=m;p=m-x+1;}
int tmp=(n-p)/gap;
tt=tmp*2;p+=gap*tmp;
while(p<n||ps!=y){
++tt;
if(ps==1){
if(p+y-1>=n){p+=y-1;break;}
p+=m-1;ps=m;
continue;
}
if(ps==m){
if(p+m-y>=n){p+=m-y;break;}
p+=m-1;ps=1;
continue;
}
}
if(res>tt) res=tt,cnt=0;
if(res==tt){
int d=(p-n)>>1;
cnt+=C(d+tt-1,d);
if(cnt>=P) cnt-=P;
}
}
printf("%d %d\n",res+1,cnt);
}
else puts("0 0");
return;
}
}
int main(){
n=read();m=read();q=read();lim=(m+1)<<1;
for(int i=0;i<lim;++i) F.f[i]=G.f[i]=0;
F.f[0]=G.f[0]=G.f[1]=G.f[lim-1]=1;
fac[0]=1;
for(int i=1;i<=m;++i) fac[i]=(ll)fac[i-1]*i%P;
fiv[m]=qp(fac[m]);
for(int i=m;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
int ex=n-1;
while(ex){
if(ex&1) F=F*G;
G=G*G;ex>>=1;
}
while(q--) solve();
return 0;
}
XX Open Cup, Grand Prix of Tokyo, F: Robots
考虑你从左向右匹配就是最优的。
但是你不能操作机器人,怎么办?
从左到右扫,你把你操作不来的点压到栈里,然后再考虑你操作一个点时栈顶节点是否一定可以被正确匹配安全弹出。
构造题好,下次 T1 还放构造。
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
char c=getchar();int x=0;
while(c<48||c>57) c=getchar();
do x=(x<<1)+(x<<3)+(c^48),c=getchar();
while(c>=48&&c<=57);
return x;
}
const int N=200003;
int n,a[N],b[N];
int stk[N],tp;
int main(){
n=read();
for(int i=1;i<=n;++i) a[i]=read();
for(int i=1;i<=n;++i) b[i]=read();
long long res=0;
for(int i=1;i<=n;++i) res+=abs(a[i]-b[i]);
printf("%lld\n",res);
for(int i=1;i<=n;++i){
if(a[i]>=b[i]){
while(tp){
int p=stk[tp];
if(b[p]-a[p]<=a[i]-b[p]){
printf("%d ",stk[tp--]);
}
else break;
}
printf("%d ",i);
}
else stk[++tp]=i;
}
while(tp) printf("%d ",stk[tp--]);
putchar('\n');
return 0;
}