日常花絮,不建议当题解看。
事情的起因是模拟赛考了这么一道题:
。
他放在了 T2,我也场切了,所以我觉得这就是个简单贪心题。但是赛后讲评人说这个题等价于 NOIP2023 T3,由于我不太相信自己能场切紫题(虽然洛谷日常虚高),且我前年赛时以及去年和今年 VP 的时候都尝试过双序列拓展,但是都失败了,也没有看过题解,所以我觉得我不可能因为他放在 T2 就切出了我尝试 \(3\) 次都不会的题。
先讲一下这个模拟赛题的做法:
题意转换一下,把 \(a\) 序列用前缀和替代,条件等价于 \(a_l\ge a_r\),那么可以把 \(a[1,k]\) 当成一个序列 \(x\),\(reverse(a[k,n])\) 当成一个序列 \(y\),现在假设 \(n,m\) 分别为序列 \(x,y\) 的长度,且维护两个指针 \(l,r\) 初始 \(l=r=1\),每次可以移动一个指针且需要始终满足 \(x_l\ge y_r\),问是否能最终让 \(l=n,r=m\)。
首先有一些显然无解的情况:\(x_1<y_1,x_n<y_m,\max(x_i)<\max(y_i),\min(x_i)<\min(y_i)\)。下面认为不会出现这 \(4\) 种情况。
一个想法是贪心地在 \(r=1\) 时让 \(l\) 尽可能往右移,直到遇到一个 \(x_u<y_r\),此时 \(x_u\) 不能再和 \(x_r\) 匹配了,因此我们需要在之前的某个 \(l=i<u\) 的时刻,把 \(r\) 往右移到一个满足 \(y_v\le x_u\) 的地方,这样才能让 \(l\) 顺利度过 \(u\) 这个地方。显然这个 \(i\) 我们会选择 \(x[1,u)\) 中最大值的位置(这样 \(r\) 能移得次数最多),这个 \(v\) 我们会选择 \(x_i\) 能让 \(r\) 移动的范围内(即移动 \(r\) 的过程中不能出现 \(y_r>x_i\) 的情况)最小值的位置。如果找不到这样的 \(v\) 显然无解,否则让 \(l=u,r=v\),继续重复这个过程。
但是有个问题,虽然我们最后可以让 \(l=n\),但是 \(r\) 不一定可以到 \(m\),事实上,最后我们至多只会把 \(r\) 移动到 \(y\) 的全局最小值的位置上,因为假设 \(pos_1\) 为 \(x\) 的全局最大值的位置,\(pos_2\) 为 \(y\) 的全局最小值的位置,那么在上面的过程中当 \(pos_1<u\) 时,\(i,v\) 分别会取到 \(pos_1,pos_2\),然后我们就会一口气把 \(l\) 移到 \(n\);即使没有出现过 \(pos_1<u\) 的情况,我们也可以在 \(l=pos_1\) 时立刻把 \(r\) 移到 \(pos_2\),这样也一定是合法的。
那怎么办呢,发现指针的移动是可逆的,所以直接把序列 \(x,y\) 都 \(reverse\) 一下再做一遍,如果 \(r\) 仍然能到全局最小值的位置那么就是合法的。
当然为了方便的话,你可以只对 \(x[1,pos_1],y[1,pos_2]\) 和 \(reverse(x[pos_1,n]),reverse(y[pos_2,m])\) 分别做一遍,看一下每次 \(r\) 是否能移到最后即可。
不难用双指针维护这个过程,复杂度线性。
贴个代码:
#include<bits/stdc++.h>
#define Debug puts("-------------------------")
#define LL long long
using namespace std;
const int N=1e5+5;
template <typename T>
inline void read(T &x){T w=1,s=0;char c=getchar();for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());return x=w*s,void();
}
int T,n,k;
LL a[N],b[N],c[N];
bool check(int n,int m){int now=1,pos=0;LL maxn=LLONG_MIN,ming=LLONG_MAX;for(int i=1,j=1;i<=n;i++){if(b[i]<c[now]){while(j<=m&&c[j]<=maxn){if(c[j]<ming) ming=c[j],pos=j;j++;}now=pos;if(b[i]<c[now]) return false;}else maxn=max(maxn,b[i]);}return true;
}
signed main(){double beg=clock();read(T);while(T--){read(n),read(k);LL mx1=LLONG_MIN,mx2=LLONG_MIN,mn1=LLONG_MAX,mn2=LLONG_MAX;for(int i=1;i<=n;i++){read(a[i]);a[i]+=a[i-1];if(i<=k) mx1=max(mx1,a[i]),mn1=min(mn1,a[i]);if(i>=k) mx2=max(mx2,a[i]),mn2=min(mn2,a[i]);}if(a[1]<a[n]||mx1<mx2||mn1<mn2){puts("No");continue;}int pos1=0,pos2=0;for(int i=1;i<=k;i++) if(a[i]==mx1){ pos1=i; break; }for(int i=k;i<=n;i++) if(a[i]==mn2){ pos2=i; break; }for(int i=1;i<=pos1;i++) b[i]=a[i];for(int i=n,j=1;i>=pos2;i--,j++) c[j]=a[i];bool flag=check(pos1,n-pos2+1);if(!flag){puts("No");continue;} for(int i=k,j=1;i>=pos1;i--,j++) b[j]=a[i];for(int i=k,j=1;i<=pos2;i++,j++) c[j]=a[i];flag=check(k-pos1+1,pos2-k+1);puts(flag?"Yes":"No");}cerr << "Time: " << (clock()-beg) << endl;return 0;
}
我赛时的另一个思考方向是,相当于要对 \(x\) 序列的每个 \(x_i\) 分配一个区间 \([L_i,R_i]\),满足 \(\forall j\in [L_i,R_i],y_j\le x_i\),且 \(L_1=1,R_n=m,L_i=R_{i-1}\)。直接做可以 \(O(n^2)\),可惜我并没有想到优化的办法。但是这个思路在下面会用到。
将信将疑之下,我再次打开双序列序列拓展,根据题意把代码改了一下(唯二的区别是双序列拓展的条件不含 \(=\),且可以是全 \(>\),也可以是全 \(<\),不过也就是取负之后做两遍而已):
#include<bits/stdc++.h>
#define Debug puts("-------------------------")
using namespace std;
const int N=5e5+5;
template <typename T>
inline void read(T &x){T w=1,s=0;char c=getchar();for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());return x=w*s,void();
}
int test_id,n,m,T;
int t1[N],t2[N],x[N],y[N],b[N],c[N];
bool check(int n,int m){int now=1,pos=0;int maxn=INT_MIN,ming=INT_MAX;for(int i=1,j=1;i<=n;i++){if(b[i]<=c[now]){while(j<=m&&c[j]<maxn){if(c[j]<ming) ming=c[j],pos=j;j++;}now=pos;if(b[i]<=c[now]) return false;}else maxn=max(maxn,b[i]);}return true;
}
bool work(){int mx1=INT_MIN,mx2=INT_MIN,mn1=INT_MAX,mn2=INT_MAX;for(int i=1;i<=n;i++) mx1=max(mx1,x[i]),mn1=min(mn1,x[i]);for(int i=1;i<=m;i++) mx2=max(mx2,y[i]),mn2=min(mn2,y[i]);if(x[1]<=y[1]||x[n]<=y[m]||mx1<=mx2||mn1<=mn2) return false;int pos1=0,pos2=0;for(int i=1;i<=n;i++) if(x[i]==mx1){ pos1=i; break; }for(int i=1;i<=m;i++) if(y[i]==mn2){ pos2=i; break; }for(int i=1;i<=pos1;i++) b[i]=x[i];for(int i=1;i<=pos2;i++) c[i]=y[i];if(!check(pos1,pos2)) return false;for(int i=n,j=1;i>=pos1;i--,j++) b[j]=x[i];for(int i=m,j=1;i>=pos2;i--,j++) c[j]=y[i];return check(n-pos1+1,m-pos2+1);
}
bool solve(){bool flag=work();for(int i=1;i<=n;i++) x[i]=-x[i];for(int i=1;i<=m;i++) y[i]=-y[i];flag|=work();return flag;
}
signed main(){double beg=clock();scanf("%d%d%d%d",&test_id,&n,&m,&T);for(int i=1;i<=n;i++) scanf("%d",&t1[i]),x[i]=t1[i];for(int i=1;i<=m;i++) scanf("%d",&t2[i]),y[i]=t2[i];string ans="";ans=ans+(solve()?'1':'0');while(T--){int kx,ky; scanf("%d%d",&kx,&ky);memcpy(x,t1,sizeof x),memcpy(y,t2,sizeof y);for(int i=1,p,v;i<=kx;i++){scanf("%d%d",&p,&v);x[p]=v;}for(int i=1,p,v;i<=ky;i++){scanf("%d%d",&p,&v);y[p]=v;} ans=ans+(solve()?'1':'0');}cout<<ans<<'\n';cerr << "Time: " << (clock()-beg) << endl;return 0;
}
可以发现核心代码 check 函数完全没有变,但是他过了。
同样转换一下题意发现双序列拓展等价于:
对 \(x\) 序列的每个 \(x_i\) 分配一个区间 \([L_i,R_i]\),满足 \(\forall j\in [L_i,R_i],y_j< x_i\),且 \(L_1=1,R_n=m,L_i=R_{i-1} or R_{i-1}+1\)。
举个例子,对于 \(x=\{ 3,2,4,2 \},y=\{ 2,1,3,1 \}\),显然可以让 \(L_i=R_i=i\),但这在上一题是不允许的。
但显然正赛数据不可能这么水,所以仔细思考后发现这两个题其实真的是等价的:
- 充分性:上一题合法的构造方案放到这一题显然是合法的。
- 必要性:我们要证明这一题合法的构造方案一定可以转换成上一题合法的构造方案,若存在 \(i,i+1\) 满足 \(R_i+1=L_{i+1}\),那么如果 \(x_i<x_{i+1}\) 则可以把 \(L_{i+1}-1\),否则可以把 \(R_i+1\)。
看了题解之后发现大家好像都是转换成网格图走路的,但是代码实现出来本质貌似是一样的。
所以考场上我该如何想到双序列拓展这个题实际上不需要考虑 \(R_i+1=L_{i+1}\) 的情况。