[冲刺国赛2022] 模拟赛6
区间第k小
题目描述
给定一个长度为 \(n\) 的序列,每个位置的值在 \([0,n)\) 这个范围中。\(q\) 次询问某一区间,将所有在区间中出现次数超过 \(w\) 的数字视为数字 \(n\),求区间第 \(k\) 小是多少。
注意:\(w\) 是一开始给定的,\(k\) 是随着询问而变化的。
\(n,q,w\leq 10^5\),强制在线。
解法
首先考虑离线怎么做(被部分分诈骗了,一直在想 \(w=1\) 怎么做),移动右端点,维护所有左端点的权值线段树。当新加入的值是 \(x\) 时,如果当前的出现次数 \(\leq w\) 可以直接在区间 \([1,i]\) 中添加 \(1\) 个 \(x\);否则找到往前 \(w\) 个数的位置 \(a\) 和往前 \(w+1\) 个数的位置 \(b\),在区间 \((a,i]\) 添加 \(1\) 个 \(x\),在区间 \((b,a]\) 中添加 \(-w\) 个 \(x\)
所以可以直接用树套树维护,注意外层的区间线段树要标记永久化,内层的权值线段树只需要单点修改。询问时取出一条链上的权值线段树,在 \(\tt log\) 棵权值线段树上二分即可。
转强制在线的方法就是把这个树套树也可持久化,注意为了让各个版本互不影响,内外层线段树都需要可持久化。这并不需要高超的技巧,只需要在修改时暴力复制即可,时间复杂度 \(O(n\log^2 n)\)
#include <cstdio>
#include <iostream>
#include <queue>
using namespace std;
const int M = 100005;
const int N = 80*M;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,w,q,ty,cnt,p[M],zz[M],rt[N],ls[N],rs[N];
queue<int> s[M];
namespace tree
{
const int N = 440*M;
int cnt,ls[N],rs[N],s[N];
int copy(int &x)
{
int y=++cnt;ls[y]=ls[x];rs[y]=rs[x];
s[y]=s[x];return y;
}
void ins(int &x,int l,int r,int y,int c)
{
x=copy(x);s[x]+=c;
if(l==r) return ;
int mid=(l+r)>>1;
if(mid>=y) ins(ls[x],l,mid,y,c);
else ins(rs[x],mid+1,r,y,c);
}
}
int copy(int &x)
{
int y=++cnt;ls[y]=ls[x];rs[y]=rs[x];
rt[y]=rt[x];return y;
}
void ins(int &x,int l,int r,int L,int R,int y,int c)
{
if(L>r || l>R) return ;
x=copy(x);
if(L<=l && r<=R)
{
tree::ins(rt[x],0,n,y,c);
return ;
}
int mid=(l+r)>>1;
ins(ls[x],l,mid,L,R,y,c);
ins(rs[x],mid+1,r,L,R,y,c);
}
int ask(int x,int y,int k)
{
static int a[30]={};int m=0,sum=0;
for(int i=x,l=1,r=n;;)
{
int mid=(l+r)>>1;
a[++m]=rt[i];
sum+=tree::s[a[m]];
if(l==r) break;
if(mid>=y) i=ls[i],r=mid;
else i=rs[i],l=mid+1;
}
//printf("%d\n",sum);
if(sum<k) return n;
for(int l=0,r=n;l<=r;)
{
if(l==r) return l;
sum=0;int mid=(l+r)>>1;
for(int i=1;i<=m;i++)
sum+=tree::s[tree::ls[a[i]]];
if(sum>=k)
{
for(int i=1;i<=m;i++)
a[i]=tree::ls[a[i]];
r=mid;
}
else
{
for(int i=1;i<=m;i++)
a[i]=tree::rs[a[i]];
l=mid+1;k-=sum;
}
}
return -1;
}
signed main()
{
freopen("kth.in","r",stdin);
freopen("kth.out","w",stdout);
n=read();w=read();q=read();ty=read();
for(int i=1;i<=n;i++)
{
int x=read();
s[x].push(i);zz[i]=zz[i-1];
if(s[x].size()<=w)
ins(zz[i],1,n,1,i,x,1);
else
{
int h=s[x].front();s[x].pop();
ins(zz[i],1,n,h+1,i,x,1);
ins(zz[i],1,n,p[x]+1,h,x,-w);
p[x]=h;
}
}
for(int i=1,ans=0;i<=q;i++)
{
int l=read(),r=read(),k=read();
if(ty) l^=ans,r^=ans,k^=ans;
ans=ask(zz[r],l,k);
printf("%d\n",ans);
}
}
树
题目描述
给定一棵 \(n\) 个点的树,若按照 \(u\rightarrow v\) 的方向经过边 \((u,v)\),如果 \(u<v\),则会让两个点的点权都增加 \(1\);如果 \(u>v\),则会让两个点的点权都减少 \(1\)
现在给定每个点最终的点权,尝试构造 \(m\) 个路径 \((x_1,y_1),(x_2,y_2)...(x_m,y_m)\),使得经过这些路径之后能对应给定的点权。在 \(m\) 最小的情况下,要求 \(x_1,y_1,x_2,y_2...x_m,y_m\) 的字典序最小。
\(n\leq 10^6\),数据保证答案的 \(m\leq n\)
解法
首先考察链的情况,发现 \(m\) 最小的策略就是能延伸多长就延伸多长,但是不能与已有的点权方向相反。(比如如果已有的点权是正,那么构造过程中就不能让它减小)
推广到树的情况就是,从叶子往上构造,用合并子树链的方法,可以方便地计算出 \(m\) 的最小值。为了保证最小字典序,我们按照字典序枚举路径,然后检测添加这条路径之后路径总数是否仍然最小,就获得了 \(O(n^3)\) 的做法。
关键的 \(\tt observation\) 是:两种方案 A->B,C->D
和 A->D,C->B
产生的效果是一样的。这说明起点和终点内部是可以任意交换的,那么我们从叶子往上求出每个点作为起点或者终点的次数,然后分别排序即可,时间复杂度 \(O(n\log n)\)
#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1000005;
#define pb push_back
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,a[M],b[M];vector<int> g[M],x,y;
void dfs(int u,int fa)
{
for(int v:g[u]) if(v^fa)
{
dfs(v,u);
a[u]-=a[v];
int t=(u<v)?a[v]:-a[v];
b[u]+=t;b[v]-=t;
}
}
signed main()
{
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
g[u].pb(v);g[v].pb(u);
}
dfs(1,0);
for(int i=1;i<=n;i++)
{
int f=b[i]>0;b[i]=b[i]>0?b[i]:-b[i];
while(b[i]--)
{
if(f) x.pb(i);
else y.pb(i);
}
}
sort(x.begin(),x.end());
sort(y.begin(),y.end());
printf("%d\n",x.size());
for(int i=0;i<x.size();i++)
printf("%d %d\n",x[i],y[i]);
}