20250409NOI模拟赛
20250409NOI模拟赛
T1.subsequence
题面:
有两个长度为 \(n\) 的由 \(\{0,1,2\}\) 构成的序列,求他们的最长不降公共子序列。\(n\leq 5\times 10^6\)
题解:
这不是一道 \(DP\) 题。
设 \(As_i,Bs_i\) 为第一个串和第二个串中 \(1\) 的个数的前缀和。
考虑枚举答案中 \(0\) 的个数 \(x\) 和 \(2\) 的个数 \(z\),可以的到两个序列中第 \(x\) 个 \(0\) 的位置是 \((i,i')\),倒数 \(z\) 个 \(2\) 的位置是 \((j,j')\),那么答案对 \(x+z+\min(As_j-As_i,Bs_{j'}-Bs_{i'})\) 取 \(\max\),限制是 \(i\leq j,i'\leq j'\)。这样 \(n^2\) 是好做的。
如果想降低复杂度必须少枚举一个东西,我们假设枚举 \(x\),发现答案的贡献式中有 \(\min\) 是不好处理的,那么考虑钦定第一个串选择的 \(1\) 的个数小于等于第二个串,统计这个情况下的答案,另一种的答案只需要 \(\rm swap\) 两个串再做一遍就行了。
设最对于一个 \(x\),\(z\) 能取到满足 \(i\leq j,i'\leq j'\) 的最大的 \(z\) 是 \(r_x\),\(O(n)\) 是好求的。
那么现在的贡献是 \(x+z+As_j-As_i=x-As_i+(z+As_j)\),限制是 \(z\leq r_x,As_j-As_i\leq Bs_{j'}-Bs_{i'}\),将第二个限制移项得 \(As_j-Bs_{j'}\leq As_i-Bs_{i'}\)。现在这转变成了一个二维数点问题,\(O(n\log n)\) 是好做的。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
int s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=5e6+6;
int n,sa[N],sb[N],ans;
pair<int,int>R[N];
char a[N],b[N];
namespace tr{
int c[N<<1],B;
void add(int x,int v){
x+=n+1;
while(x<=B){
c[x]=max(c[x],v);
x+=x&-x;
}
}
int query(int x){
x+=n+1;
int res=0;
while(x>0){
res=max(res,c[x]);
x-=x&-x;
}
return res;
}
void clear(){
for(int i=B;i>=1;i--) c[i]=0;
}
}
void calc(){
vector<int>Az,Bz,Ax,Bx;
for(int i=n;i>=1;i--){
if(a[i]=='2') Az.push_back(i);
else if(a[i]=='0'){
Ax.push_back(i);
R[Ax.size()].first=Az.size();
}
if(b[i]=='2') Bz.push_back(i);
else if(b[i]=='0'){
Bx.push_back(i);
R[Bx.size()].second=Bz.size();
}
}
R[Ax.size()+1].first=Az.size();R[Bx.size()+1].second=Bz.size();
for(int i=1;i<=n;i++){
sa[i]=sa[i-1]+(a[i]=='1');
sb[i]=sb[i-1]+(b[i]=='1');
}
tr::add(sa[n]-sb[n],sa[n]);
int z=0;
for(int x=min(Ax.size(),Bx.size());x>=0;x--){
int lim=min(R[Ax.size()-x+1].first,R[Bx.size()-x+1].second);
while(z<lim){
int i=Az[z],j=Bz[z];
z++;
tr::add(sa[i]-sb[j],z+sa[i]);
}
if(x==0) ans=max(ans,tr::query(0));
else{
int X=Ax[Ax.size()-x],Y=Bx[Bx.size()-x];
ans=max(ans,tr::query(sa[X]-sb[Y])-sa[X]+x);
}
}
tr::clear();
}
void solve(){
ans=0;
scanf("%s%s",a+1,b+1);
n=strlen(a+1);
tr::B=n+n+1;
calc();
swap(a,b);
calc();
printf("%d\n",ans);
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int T=read();
while(T--) solve();
return 0;
}
T2.constructive
题面:
给定一个 \(n\) 个点 \(m\) 的有向图,给每个点赋一个整数值 \(val_i\in[1,n+1)\),要求 \(\forall (u\to v),val_u<val_v\),最小化 \(\sum_{(u\to v)}val_v-val_u\)。求最小值或判断无解。\(n\leq 300,m\leq 1500\) 有 \(60pts\) 部分分 \(n\leq 30,m\leq 30\)。
题解:
题目的答案是 \(\sum_i val_i\times (ind_i-oud_i)\)。
\(60pts\) 的做法:
看数据范围想网络流。需要给每个点赋值有套路是每个点建 \(n\) 个点,第 \(i\) 的点建出来的 \(n\) 个点中的第 \(j\) 个点 \(ids(i,j)\) 表示 \(val_i\leq j\),割掉了 \(e=(ids(i,j)\to ids(i,j+1))\) 表示 \(val_i=j\)。
连边 \(ids(i,j)\to ids(i,j+1)\),边权是 \(j\times (ind_i-oud_i)\),但是由于网络流不能有负权,所以可以设成 \(j\times ind_i+(n-j)\times oud_i\),最后再减掉。
对于原图的边 \(e=(x\to y)\) 建边 \(ids(x,i)\to ids(y,i+1)\),不允许割断。
\(S\) 连所有 \(ids(i,1)\),\(ids(i,n+1)\) 相当于是 \(T\),跑最小割。答案是 \(dinic()-nm\)。
满分做法是线性规划然后费用流,可惜我不会。
60 分的代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=305,M=1505;
const int inf=1e9+7;
int dfn[N],low[N],tim,nsc,n,m,val[N],ind[N],oud[N];
stack<int>st;
bool vis[N];
vector<int>G[N];
void tarjan(int x){
vis[x]=1;st.push(x);
dfn[x]=low[x]=++tim;
for(int v:G[x])
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
}
else if(vis[v]) low[x]=min(low[x],dfn[v]);
if(dfn[x]==low[x]){
nsc++;
while(!st.empty()){
int v=st.top();
vis[v]=0;
st.pop();
if(v==x) break;
}
}
}
namespace flow{
int head[N*N],E[N*N],dep[N*N],cnt=1,S,T;
struct edge{
int v,nxt,w;
}e[N+N*N+N+N*M<<1];
int ids(int x,int y){
return (x-1)*n+y;
}
void add(int u,int v,int w){
// cout<<u<<' '<<v<<' '<<w<<endl;
e[++cnt]={v,head[u],w};
head[u]=cnt;
e[++cnt]={u,head[v],0};
head[v]=cnt;
}
bool bfs(){
for(int i=S;i<=T;i++) dep[i]=0,E[i]=head[i];
dep[S]=1;
queue<int>q;
q.push(S);
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].v;
if(!dep[v]&&e[i].w){
dep[v]=dep[x]+1;
q.push(v);
if(v==T) return 1;
}
}
}
return 0;
}
int dfs(int x=S,int W=inf){
if(x==T) return W;
int now=0;
for(int i=E[x];i&&now<W;i=e[i].nxt){
E[x]=i;int v=e[i].v;
if(dep[v]==dep[x]+1&&e[i].w){
int tmp=dfs(v,min(e[i].w,W-now));
if(!tmp) dep[v]=-1;
e[i].w-=tmp;
e[i^1].w+=tmp;
now+=tmp;
if(now==W) return W;
}
}
return now;
}
ll dinic(){
ll ans=0; int x=0;
while(bfs()) while(x=dfs()) ans+=x;
return ans;
}
void calc(){
S=0;T=n*n+1;
for(int i=1;i<=n;i++){
add(S,ids(i,1),inf);
for(int j=1;j<n;j++) add(ids(i,j),ids(i,j+1),j*ind[i]+(n-j)*oud[i]);
if(oud[i]==0) add(ids(i,n),T,n*ind[i]);
else add(ids(i,n),T,inf);
for(int v:G[i])
for(int j=1;j<n;j++) add(ids(i,j),ids(v,j+1),inf);
}
printf("%lld\n",dinic()-n*m);
queue<int>q;
for(int i=S;i<=T;i++) dep[i]=0;
dep[S]=1;
q.push(S);
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x],v;i;i=e[i].nxt){
v=e[i].v;
if(e[i].w>0&&!dep[v]){
dep[v]=1;
q.push(v);
}
}
}
for(int i=1;i<=n;i++){
if(dep[ids(i,n)]==1){
val[i]=n;
continue;
}
for(int j=n;j>1;j--)
if(dep[ids(i,j-1)]==1&&dep[ids(i,j)]==0){
val[i]=j-1;
break;
}
}
int mn=*min_element(val+1,val+1+n);
for(int i=1;i<=n;i++) val[i]-=mn-1;
for(int i=1;i<=n;i++) printf("%d ",val[i]);
puts("");
for(int i=S;i<=T;i++) head[i]=0;cnt=1;
}
}
using flow::calc;
void solve(){
n=read();m=read();
for(int i=1;i<=n;i++) dfn[i]=low[i]=0,ind[i]=oud[i]=0;
for(int i=1;i<=n;i++) vis[i]=0;
for(int i=1;i<=n;i++) G[i].clear();
while(!st.empty()) st.pop();
tim=0;nsc=0;
for(int i=1;i<=m;i++){
int u=read(),v=read();
G[u].push_back(v);
ind[v]++;oud[u]++;
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
if(nsc<n){
puts("NIE");
return ;
}
calc();
}
int main(){
// freopen("constructive.in","r",stdin);
// freopen(".out","w",stdout);
int T=read();
while(T--) solve();
return 0;
}
T3.string
题面:
定义将一个串划分成 \(k\) 段的价值是这 \(k\) 段中字典序最大的段。
给定一个长为 \(n\) 的串 \(S\),\(q\) 次询问,每次询问给出 \(i,k\),回答将 \(S[i:n]\) 划分成至多 \(k\) 段的所有方案中最小的价值是多少。输出 \(l,r\) 表示最小的价值是 \(S[l:r]\) 这个字符串,多个答案输出 \(l\) 最小的。\(n,q\leq 10^6\)
题解:
先考虑如果没有后缀的限制,每次询问只给出 \(k\) 怎么做。
将 \(S\) \(\rm lyndon\) 分解,得 \(S=\sum_i s_i^{p_i}\),选别的位置分段一定不如选 \(\rm lyndon\) 串的开头优。
根据 \(\rm lyndon\) 分解的性质有 \(s_i>s_{i+1}\),所以直接将 \(S\) 分解成 \(S=s_1^{p_1}+S'\) 不影响答案,具体可以看答案的分讨结果。
- 若 \(k=1\),答案是 \(l=1,r=|S|\)。
只分 \(1\) 段显然只能全选。
- 若 \(k\geq p_1+[S'\not= \emptyset]\),答案是 \(l=1,r=|s_1|\)。
根据 \(\rm lyndon\) 分解,\(s_1>S'\) 而 \(s_1^k>s_1^{k-1}\),所以将每个 \(s_1\) 单独分段,最大的就是 \(s_1\)。
- 若 \(k\nmid p_1\) 或 \(S'=\emptyset\) 时,答案是 \(l=1,r=\lceil\dfrac{p_1}{k}\rceil\times |s_1|\)。
除了上述两种情况,在其他时候我们应该尽可能将 \(p_1\) 段 \(s_1\) 平均分。设 \(d=\lceil\dfrac{p_1}{k}\rceil\),\(S\) 将分成若干段的 \(s_1^d\) 和若干 \(s_1^{d-1}\)。\(S'\) 接到余下的某个 \(s_1^{d-1}\) 的后面,最大的仍然是 \(s_1^d\)。(因为 \(k\nmid p_1\),所以一定有 \(s_1^{d-1}\),或者说是 \(S'=\emptyset\) 不用接)
- 若 \(k\mid p_1\) 且 \(S'\not=\emptyset\) 时,答案是 \(l=(k-1)\times \frac{p_1}{k}\times |s_1|+1,r=|S|\)。
第三种情况在大部分时候是正确的,但在符合以上条件的特例时是不对的,因为这时可能没有 \(s_1^{d-1}\) 的串,那就只能接到某个 \(s_1^d\) 后面,所以答案是 \(s_1^d+S'\)。
那现在询问可以离线,只需要考虑怎么求出每个后缀的 \(\rm lyndon\) 分解就行了。
正常的 \(Duval\) 算法是做不到这一点的,考虑用 \(SA\) 来做。(当然可以二分哈希代替)
\(SA\) 做正常的 \(\rm lyndon\) 分解怎么做?
\(\rm lyndon\) 串满足其所有后缀中它是最小的,那么求出 \(SA\) 之后从小到大遍历后缀 \(sa[i]\),如果没有被覆盖那就选择它是一个新的 \(\rm lyndon\) 串的开头,并覆盖 \([sa[i],n]\)。
后缀怎么做?
设我们现在求出了 \([i+1,n]\) 的分解,现在加入一个后缀 \(S[i:n]\),他的排名是 \(rk[i]\),那么在 \(i+1\) 的 \(\rm lyndon\) 分解中 \(rk[j]>rk[i]\) 的都不应该出现在 \(i\) 的分解中,而且以后都不会出现在 \(\rm lyndon\) 分解中。可以使用单调栈实现这一点。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=1e6+5;
int rk[N],c[N],sa[N],id[N],tmp[N],n,m,f[N];
int h[N],lgn[N],st[21][N];
pair<int,int> ans[N];
vector<pair<int,int> >upd[N];
char s[N];
int find(int l,int r){
if(l>r) swap(l,r);
l++;
int mn=lgn[r-l+1];
return min(st[mn][l],st[mn][r-(1<<mn)+1]);
}
void SA(){
m=26;
for(int i=1;i<=n;i++) rk[i]=s[i]-'a'+1;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[i]]--]=i;
int p=0;
for(int k=1;;k<<=1,m=p){
int cnt=0;
for(int i=n-k+1;i<=n;i++) id[++cnt]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) id[++cnt]=sa[i]-k;
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[rk[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[rk[id[i]]]--]=id[i];
for(int i=1;i<=n;i++) tmp[i]=rk[i];
p=0;
for(int i=1;i<=n;i++){
if(tmp[sa[i]]!=tmp[sa[i-1]]||tmp[sa[i]+k]!=tmp[sa[i-1]+k]) ++p;
rk[sa[i]]=p;
}
if(p==n) break;
}
p=0;
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;
if(p) p--;
int j=sa[rk[i]-1];
while(i+p<=n&&j+p<=n&&s[i+p]==s[j+p]) p++;
h[rk[i]]=p;
}
for(int i=2;i<=n;i++) lgn[i]=lgn[i>>1]+1;
for(int i=1;i<=n;i++) st[0][i]=h[i];
for(int j=1;j<=lgn[n];j++)
for(int i=1;i+(1<<j)-1<=n;i++)
st[j][i]=min(st[j-1][i],st[j-1][i+(1<<j-1)]);
}
int main(){
// freopen("string.in","r",stdin);
// freopen("string.out","w",stdout);
scanf("%s",s+1);
n=strlen(s+1); SA();
int Q=read();
for(int i=1;i<=Q;i++){
int l=read(),k=read();
upd[l].push_back({i,k});
}
vector<int>vec;
vec.push_back(n+1);
for(int i=n;i>=1;i--){
while(vec.size()>1&&rk[i]<rk[vec.back()]) vec.pop_back();
vec.push_back(i);
f[i]=1;
if(vec.size()>2){
int x=i,y=vec.end()[-2],z=vec.end()[-3];
if(y-x==z-y&&find(rk[x],rk[y])>=y-x) f[x]+=f[y];
}
for(auto o:upd[i]){
int id,k; tie(id,k)=o;
int now=min(f[i]+1,((int)vec.size())-1);
if(k==1) ans[id]={i,n};
else if(k>=now) ans[id]={i,vec.end()[-2]-1};
else{
int len=vec.end()[-2]-i;
if(now==f[i]+1&&now%k==1) ans[id]={i+(now-1)/k*(k-1)*len,n};
else ans[id]={i,i+((now-1)/k+1)*len-1};
}
}
}
for(int i=1;i<=Q;i++) printf("%d %d\n",ans[i].first,ans[i].second);
return 0;
}