MX 暑假集训 7.25
\(100+40+5\),呜呜呜我破防了,T2 想了一个小时想出正解,但是发现要维护一个分成三段的分段函数的复合,再上个简单的 KDTree,写了两个小时没调出来函数的各种操作,KDTree 也就没打。呜呜呜。
T1: [ROIR 2025] 酸雨
题意
给定 \(n\) 个模块,初始时第 \(i\) 个模块高度为 \(h_i\),有 \(n-1\) 次合并操作,每次合并操作给定一个 \(k\),将第 \(k\) 个和第 \(k+1\) 个模块合并为一个模块,每次合并后,输出如果给这个模块倒满水,共能够留下多少体积的水,即令这个模块的最左边下标与最右边下标分别为 \(l,r\),求 \(\sum\limits_{i=l}^{r}(\min(\max\limits_{j=l}^{i}h_j,\max\limits_{j=i}^{r}h_j)-h_i)\)。
\(1\le n\le 10^5\)。
solution
这不是我们的 [SNOI2020] 水池 超级弱化版吗,秒了。
合并两个模块,那么我们找到两个模块中最大值较小的模块,那么这个模块的最大值向另一个模块之间可以多留住一些水。
我们钦定左边的模块最大值比右边小,左边模块最大值位置为 \(l\),那么找到右边模块中从左到右第一个位置 \(r\) 满足 \(h_r\ge h_l\),那么 \([l,r]\) 这段区间留下的水面高度即为 \(h_l\),除了这一段区间,其他位置能留下的水面高度显然没有影响,所以用个线段树二分和线段树维护区间和即可。
还有更优秀的做法,找到第 \(k\) 个模块需要 \(O(n\log n)\),其余部分可以用单调栈线性解决,不过还是太吃思维了。
写了三棵线段树,有点长但很好写很好调,不到一个小时写完了。
时间复杂度 \(O(n\log n)\)。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n;
int h[N];
struct Segment_Tree1 {
ll s[N<<2],tg[N<<2];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void modify(int x,int l,int r,int k) {
s[x]=1ll*(r-l+1)*k;
tg[x]=k;
}
inline void push_down(int x,int l,int r) {
if (!tg[x]) return;
int mid=l+r>>1,k=tg[x];tg[x]=0;
modify(ls(x),l,mid,k);
modify(rs(x),mid+1,r,k);
}
inline void update(int x,int y,int l,int r,int now,int k) {
if (x<=l&&y>=r) return modify(now,l,r,k);
push_down(now,l,r);
int mid=l+r>>1;
if (x<=mid) update(x,y,l,mid,ls(now),k);
if (y>mid) update(x,y,mid+1,r,rs(now),k);
s[now]=s[ls(now)]+s[rs(now)];
}
inline ll qsum(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return s[now];
push_down(now,l,r);
int mid=l+r>>1;ll sum=0;
if (x<=mid) sum=qsum(x,y,l,mid,ls(now));
if (y>mid) sum+=qsum(x,y,mid+1,r,rs(now));
return sum;
}
}tr1;
struct Segment_Tree2 {
int mx[N<<2],tg[N<<2];
ll s[N<<2],sum[N<<2];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void build(int l,int r,int now) {
if (l==r) return mx[now]=s[now]=h[l],void();
int mid=l+r>>1;
build(l,mid,ls(now));
build(mid+1,r,rs(now));
mx[now]=max(mx[ls(now)],mx[rs(now)]);
s[now]=s[ls(now)]+s[rs(now)];
}
inline void modify(int x) {sum[x]=s[x];tg[x]=1;}
inline void push_down(int x) {
if (!tg[x]) return;
int k=tg[x];tg[x]=0;
modify(ls(x));modify(rs(x));
}
inline void update(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return modify(now);
push_down(now);
int mid=l+r>>1;
if (x<=mid) update(x,y,l,mid,ls(now));
if (y>mid) update(x,y,mid+1,r,rs(now));
sum[now]=sum[ls(now)]+sum[rs(now)];
}
inline int qnxt(int x,int y,int l,int r,int now,int k) {
if (mx[now]<k) return 0;
if (l==r) return l;
int mid=l+r>>1;
if (x<=mid&&y>mid) {
int ret=qnxt(x,y,l,mid,ls(now),k);
if (ret) return ret;
return qnxt(x,y,mid+1,r,rs(now),k);
}
if (x<=mid) return qnxt(x,y,l,mid,ls(now),k);
return qnxt(x,y,mid+1,r,rs(now),k);
}
inline int qpre(int x,int y,int l,int r,int now,int k) {
if (mx[now]<k) return 0;
if (l==r) return l;
int mid=l+r>>1;
if (x<=mid&&y>mid) {
int ret=qpre(x,y,mid+1,r,rs(now),k);
if (ret) return ret;
return qpre(x,y,l,mid,ls(now),k);
}
if (x<=mid) return qpre(x,y,l,mid,ls(now),k);
return qpre(x,y,mid+1,r,rs(now),k);
}
inline ll qsum(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return sum[now];
push_down(now);
int mid=l+r>>1;ll ret=0;
if (x<=mid) ret=qsum(x,y,l,mid,ls(now));
if (y>mid) ret+=qsum(x,y,mid+1,r,rs(now));
return ret;
}
}tr2;
struct Segment_Tree3 {
int mx[N<<2],tg[N<<2];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void build(int l,int r,int now) {
mx[now]=r;
if (l==r) return;
int mid=l+r>>1;
build(l,mid,ls(now));
build(mid+1,r,rs(now));
}
inline void modify(int x,int k) {mx[x]+=k;tg[x]+=k;}
inline void push_down(int x) {
if (!tg[x]) return;
int k=tg[x];tg[x]=0;
modify(ls(x),k);
modify(rs(x),k);
}
inline void update(int x,int y,int l,int r,int now,int k) {
if (x<=l&&y>=r) return modify(now,k);
push_down(now);
int mid=l+r>>1;
if (x<=mid) update(x,y,l,mid,ls(now),k);
if (y>mid) update(x,y,mid+1,r,rs(now),k);
mx[now]=max(mx[ls(now)],mx[rs(now)]);
}
inline int qid(int l,int r,int now,int k) {
if (l==r) return l;
push_down(now);
int mid=l+r>>1;
if (mx[ls(now)]>=k) return qid(l,mid,ls(now),k);
return qid(mid+1,r,rs(now),k);
}
}tr3;
int L[N],R[N],mx[N],pmx[N],f[N];
inline int find(int x) {
if (f[x]==x) return x;
return f[x]=find(f[x]);
}
inline void solve(int idx) {
int x=find(tr3.qid(1,n,1,idx)),y=find(R[x]+1);
if (mx[x]<mx[y]) {
int l=pmx[x],r=tr2.qnxt(L[y],R[y],1,n,1,mx[x]);
if (r!=l+1) {
tr1.update(l+1,r-1,1,n,1,mx[x]);
tr2.update(l+1,r-1,1,n,1);
}
}
else {
int r=pmx[y],l=tr2.qpre(L[x],R[x],1,n,1,mx[y]);
if (r!=l+1) {
tr1.update(l+1,r-1,1,n,1,mx[y]);
tr2.update(l+1,r-1,1,n,1);
}
mx[y]=mx[x];pmx[y]=pmx[x];
}
tr3.update(L[y],n,1,n,1,-1);
f[x]=y;L[y]=L[x];
printf("%lld\n",tr1.qsum(L[y],R[y],1,n,1)-tr2.qsum(L[y],R[y],1,n,1));
}
int main() {
read(n);
f(i,1,n) {
read(h[i]);
L[i]=R[i]=f[i]=pmx[i]=i;
mx[i]=h[i];
}
tr2.build(1,n,1);
tr3.build(1,n,1);
f(i,1,n-1) {
int k;read(k);
solve(k);
}
return 0;
}
T2: 特别的人
题意
有 \(n\) 种人,第 \(i\) 种人有权值 \(a_i\) 和人数 \(c_i\),\(q\) 次询问,每次询问给定 \(l,r,x,y\),你需要将种类编号在 \([l,r]\) 之间,且权值在 \([x,y]\) 之间的人依次取出形成一个序列,每个人可以与得到的序列上相邻两种人中的一个人进行配对,每个人最多与一个人配对,问取出形成的序列中最多有多少组配对的人。
\(1\le n,q\le 1.2\times 10^5\)。
solution
讲一下考场上没调完的做法。
首先有一个显然的暴力,将所有人取出来后,从左到右扫,扫到第 \(i\) 种人时考虑第 \(i-1\) 种人还剩下多少人没配对,贪心地将第 \(i\) 种人尽可能多地与第 \(i-1\) 种人配对,剩下的第 \(i\) 种人留给第 \(i+1\) 种人配对,这样显然不劣。
然后我们考虑如何快速计算一个区间的答案,可以用扫描线维护每个询问剩余未匹配人数,然后平衡树维护,类似于 T-Shirts,还有一种思路是考虑用线段树维护一个区间的信息,由于我们合并两个线段树区间是将左区间剩余未匹配的人扔进右区间进行匹配,再从右区间中得到一些未匹配的人,令 \(f(x)\) 表示若有 \(x\) 个人从左端点开始进行匹配,右端点会剩余多少个未匹配的人,\(g(x)\) 表示若有 \(x\) 个人从左端点开始进行匹配,那么这个区间最多能够贡献多少对人。
初始时 \(f(x)\) 与 \(g(x)\) 都是分成两段的分段函数,合并左右区间时,令 \(fL(x)\) 为左区间对应 \(f(x)\) 函数,\(fR(x)\) 为右区间对应 \(f(x)\) 函数,那么最终得到这个区间的 \(f(x)\) 函数即为 \(fR(fL(x))\),也就是我们要对这个分段函数做一个复合,\(g(x)\) 同理,可以证明复合后的函数仍是分段函数,并且段数不超过 \(3\),于是我们就得到了一个支持 \(O(\log n)\) 询问一段连续区间的答案。
尝试扩展到二维,之前的扫描线思想就派上用场了,考虑从左到右对每个人进行扫描线,对于一个询问 \((l,r,x,y)\),在扫到 \(l\) 时将其挂到二维平面上的点 \((x,y)\),每扫到一个元素 \((i,a_i,c_i)\),那么对于所有二维平面上 \(x\le a_i\le y\) 的点 \((x,y)\),都会进行关于 \(c_i\) 的一个函数的复合,使用 KD-Tree 维护,每次进行矩形复合即可,扫到 \(r\) 时就可以将这个询问拿出来回答了。
函数复合是可以做到 \(O(1)\) 的,只不过情况很多需要讨论很多情况,KD-Tree 的时间复杂度为 \(O(n\sqrt q+q\log n)\),口胡一下行了代码还是极其难写的。
题解给了个更简单的做法。
首先每种人只能序列上相邻的两种人进行匹配,那么我们可以转化为一个带有点容量限制的二分图匹配问题,根据 König 定理的带容量版本,最大权匹配等于最小权点覆盖,所以原问题就转化为了对于一个矩形上的点按顺序连成一条链的最小权点覆盖,这里的证明要用线性规划 我不是很懂所以不讲了。
我们可以用 dp 来求出最小点覆盖,设计矩阵 \((dp_{i,0},dp_{i,1})\),表示考虑到第 \(i\) 个点,其中第 \(i\) 个点选或不选的最小点覆盖权值和,考虑用一个 min-plus 矩阵维护转移过程。
对于有 \(c\) 个人的一个种类,定义转移矩阵:
那么对于每个询问我们要求的就是二维平面上矩形内所有转移矩阵按照编号从小到大依次相乘所得到的矩阵,由于初始时没有限制,所以答案即为最终得到的矩阵四个位置中的最小值。
怎么快速求出这个矩阵呢,用上文提到的 KD-Tree 扫描线是可以的,题解还给了一种常数更小的分块做法。
考虑对序列分成 \(B\) 个块,每个块预处理出 \(a_i\) 在 \([l,r]\) 内的矩阵相乘得到的结果,因为这个块内实际在值域上只有 \(B\) 个位置有值,所以离散化完每个块维护的信息就是 \(O(B^2)\),那么每个询问对于散块暴力乘过去,整块通过预处理得到的矩阵 \(O(1)\) 乘过去即可。
如何快速处理出每个块中 \(a_i\) 在 \([l,r]\) 内所有的矩阵按照编号顺序相乘得到的矩阵呢?[Ynoi2013] D2T2 给出了一个分治做法,对序列分治,处理出左区间和右区间的 \([l,r]\) 的信息,乘起来即可,左右区间实际上有用的信息也只与其长度相关,那么这个分治的复杂度就是 \(T(k)=2T(\frac{k}{2})+O(k^2)=O(k^2)\) 的。
实现时卡空间,需要离线询问每个块逐块处理,类似于 [Ynoi2018] 五彩斑斓的世界。
取 \(B=\sqrt n\),时间复杂度 \(O((n+q)\sqrt n)\),空间复杂度 \(O(n+q)\)。
Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int B=300,LB=B+10;
constexpr ll inf=1e18+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n,m;
int a[N],c[N];
struct ques{int l,r,x,y;}p[N];
struct Matrix {
ll v[2][2];
inline Matrix (){v[0][0]=v[1][1]=0;v[0][1]=v[1][0]=inf;}
inline Matrix (ll a,ll b,ll c,ll d){v[0][0]=a;v[0][1]=b;v[1][0]=c;v[1][1]=d;}
inline Matrix operator *(const Matrix &x)const {
Matrix res;
res.v[0][0]=min(inf,min(v[0][0]+x.v[0][0],v[0][1]+x.v[1][0]));
res.v[0][1]=min(inf,min(v[0][0]+x.v[0][1],v[0][1]+x.v[1][1]));
res.v[1][0]=min(inf,min(v[1][0]+x.v[0][0],v[1][1]+x.v[1][0]));
res.v[1][1]=min(inf,min(v[1][0]+x.v[0][1],v[1][1]+x.v[1][1]));
return res;
}
}ans[N],tmp[LB][LB];
struct did_Pos{int l,r;};
inline vector<did_Pos> merge(vector<did_Pos> x,vector<did_Pos> y) {
for (did_Pos k:y) x.push_back(k);
return x;
}
inline vector<did_Pos> dfs(int l,int r) {
vector<did_Pos> ret;
if (l==r) {
tmp[a[l]][a[l]]=Matrix(inf,c[l],0,c[l]);
ret.push_back({a[l],a[l]});
return ret;
}
int mid=l+r>>1;
ret=merge(dfs(l,mid),dfs(mid+1,r));
struct Node{int v,o;};
vector<Node> vec;
f(i,l,mid) vec.push_back({a[i],0});
f(i,mid+1,r) vec.push_back({a[i],1});
sort(vec.begin(),vec.end(),[&](Node x,Node y){return x.v<y.v;});
int sz=vec.size()-1;
static Matrix rmp[LB][LB];
for (did_Pos k:ret) rmp[k.l][k.r]=tmp[k.l][k.r];
f(i,0,sz) {
int Lx=0,Ly=0,Rx=0,Ry=0,vi=vec[i].v;
f(j,i,sz) {
int vj=vec[j].v;
if (vec[j].o) {if (!Rx) Rx=vj;Ry=vj;}
else {if (!Lx) Lx=vj;Ly=vj;}
if (Lx&&Rx) {
tmp[vi][vj]=rmp[Lx][Ly]*rmp[Rx][Ry];
ret.push_back({vi,vj});
}
}
}
return ret;
}
inline void solve(int L,int R) {
static bitset<N> vis;
static int val[N],b[N];
vis.reset();
int mx=0,mn=n+1,cnt=0;
f(i,L,R) vis.set(b[i]=a[i]),mx=max(mx,a[i]),mn=min(mn,a[i]);
f(i,1,n) {
if (vis[i]) val[i]=++cnt;
else val[i]=val[i-1];
}
f(i,L,R) a[i]=val[a[i]];
dfs(L,R);
f(idx,1,m) {
int l=p[idx].l,r=p[idx].r,x=p[idx].x,y=p[idx].y;
if (l>R||L>r||x>mx||y<mn) continue;
if (l<=L&&R<=r) ans[idx]=ans[idx]*tmp[val[x]+(!vis[x])][val[y]];
else {
int pL=max(L,l),pR=min(R,r);
f(i,pL,pR) if (x<=b[i]&&b[i]<=y) ans[idx]=ans[idx]*Matrix(inf,c[i],0,c[i]);
}
}
}
int main() {
freopen("mingle.in","r",stdin);
freopen("mingle.out","w",stdout);
read(n);read(m);
f(i,1,n) read(a[i]);
f(i,1,n) read(c[i]);
f(i,1,m) {
int l,r,x,y;
read(l);read(r);read(x);read(y);
p[i]={l,r,x,y};
}
for (int l=1;l<=n;l+=B) solve(l,min(l+B-1,n));
f(i,1,m) printf("%lld\n",min({ans[i].v[0][0],ans[i].v[0][1],ans[i].v[1][0],ans[i].v[1][1]}));
return 0;
}

浙公网安备 33010602011771号