线段树分治 学习笔记
洛谷 P4585 [FJOI2015] 火星商店问题
简要题意
一条街道上有 \(n\) 个商店,每个商店自始至终都会售卖一个特殊商品,商店 \(i\) 的特殊商品价格为 \(v_i\)。
记初始时的时间为第 \(1\) 天。\(q\) 次操作,每次操作为以下两种类型之一:
- \(0~s~v\):时间来到了新的一天,编号为 \(s\) 的商店新购进了一种价格为 \(v\) 的商品。
- \(1~l~r~x~d\):表示一个人当日在编号在 \([l, r]\) 的商店内购买商品,他购买的商品要么是该商店的特殊商品,要么是该商店在 \(d\) 天内(包括 \(d\) 天前)进货的商品。他的喜好密码为 \(x\),也就是说你需要求出他能购买的商品中,价格 \(v\) 与 \(x\) 的异或和的最大值。
\(0 \leq n, q, v_i \leq 10^5\)。
我们将商品分为两类,第一类为特殊商品,第二类为普通商品,然后对两类商品分别求最大值。
对于第一类商品,序列是静态的,我们需要支持查询区间异或最大值,直接使用可持久化 Trie 即可。
对于第二类商品,我们将操作离线下来,将每个商品以进货时间为横坐标、所属商店为纵坐标表示在平面直角坐标系上。显然坐标系是静态的,那么我们需要支持查询矩形异或最大值,不难发现这是第一类商品的二维版本。
一维版本我们可以使用可持久化 Trie,那么二维版本怎么办呢?由于每个点对询问的贡献是独立的,考虑对纵坐标线段树分治,将查询的每个矩形水平切成 \(\Theta(\log n)\) 个小矩形,然后插到线段树上。对线段树的每个结点 \([l, r]\),我们对所有纵坐标为 \([l, r]\) 的查询矩形求答案,那么此时只需将纵坐标在 \([l, r]\) 内的商品按横坐标排序,然后这个问题就变成了一维的版本,直接使用可持久化 Trie 即可。
时间复杂度为 \(\Theta(q \log n \log V)\)。
代码
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
const int N=100003,M=17,log_N=18;
struct Point{
int a,b,x;
bool operator<(const Point u)const{
return b<u.b;
}
}a[N];
struct Question{
int al,bl,ar,br,x,p;
}b[N];
int n,m,q,c[N],root[N],res[N];
vector<Point> alls;
int binary_le(int x){
int l=-1,r=alls.size()-1;
while(l<r){
int mid=(l+r+1)/2;
(alls[mid].b<x)?(l=mid):(r=mid-1);
}
return l;
}
int binary_leq(int x){
int l=-1,r=alls.size()-1;
while(l<r){
int mid=(l+r+1)/2;
(alls[mid].b<=x)?(l=mid):(r=mid-1);
}
return l;
}
namespace Trie{
int len_node,node[N*(M+1)][2];
void clear(){
len_node=1,node[1][0]=node[1][1]=0;
}
int copy(int u){
int cur=++len_node;
node[cur][0]=node[u][0],node[cur][1]=node[u][1];
return cur;
}
void insert(int u,int v,int x){
for(int i=M-1;i>=0;i--){
if(node[u][x>>i&1]==node[v][x>>i&1])
node[u][x>>i&1]=copy(node[v][x>>i&1]);
u=node[u][x>>i&1],v=node[v][x>>i&1];
}
}
int query(int p,int q,int x){
int ans=0,i;
for(i=M-1;i>=0;i--){
if(node[q][!(x>>i&1)]!=node[p][!(x>>i&1)])
ans|=1<<i,p=node[p][!(x>>i&1)],q=node[q][!(x>>i&1)];
else
p=node[p][x>>i&1],q=node[q][x>>i&1];
}
return ans;
}
}
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_N)+3];
vector<Question> arr[(1<<log_N)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,const Question& x){
if(l>r||r<node[u].l) return ;
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,const Point a[N],int res[N]){
int i,s1;
alls.resize(node[u].r-node[u].l+2),alls[0].b=0;
for(i=node[u].l;i<=node[u].r;i++)
alls[i-node[u].l+1]=a[i];
sort(alls.begin(),alls.end());
Trie::clear(),root[0]=1;
for(i=1;i<alls.size();i++){
root[i]=Trie::copy(root[i-1]);
Trie::insert(root[i],root[i-1],alls[i].x);
}
for(i=0;i<arr[u].size();i++){
s1=Trie::query(root[binary_le(arr[u][i].bl)],root[binary_leq(arr[u][i].br)],arr[u][i].x);
res[arr[u][i].p]=max(res[arr[u][i].p],s1);
}
if(node[u].l<node[u].r) solve(u*2,a,res),solve(u*2+1,a,res);
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
void write(int x){
char a[12]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar('\n');
}
int main(){
// freopen("Martian.in","r",stdin);
// freopen("Martian.out","w",stdout);
int i,s1,len; n=read(),len=read();
for(i=1;i<=n;i++) c[i]=read();
for(i=len;i>0;i--)
switch(read()){
case 0: m++,a[m].a=m,a[m].b=read(),a[m].x=read(); break;
case 1:
q++,b[q].bl=read(),b[q].br=read(),b[q].p=q;
b[q].x=read(),s1=read(),b[q].ar=m,b[q].al=m-s1+1;
}
Trie::clear(),root[0]=1;
for(i=1;i<=n;i++){
root[i]=Trie::copy(root[i-1]);
Trie::insert(root[i],root[i-1],c[i]);
}
for(i=1;i<=q;i++)
res[i]=Trie::query(root[b[i].bl-1],root[b[i].br],b[i].x);
SGT::build(1,1,m);
for(i=1;i<=q;i++)
SGT::insert(1,b[i].al,b[i].ar,b[i]);
SGT::solve(1,a,res);
for(i=1;i<=q;i++) write(res[i]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
洛谷 P7220 [JOISC2020] 掃除
简要题意
Bitaro 居住在平面直角坐标系上。具体而言,Bitaro 的房间是一个等腰直角三角形,其三边分别为 \(y\) 轴、\(x\) 轴、直线 \(x + y = p\)。
一天,Bitaro 准备打扫房间。一开始房间内有 \(n\) 堆灰尘,第 \(i\) 堆灰尘的坐标为 \((x_i, y_i)\),同一位置可能存在多堆灰尘。接下来会依次发生 \(q\) 个事件,每个事件为以下 \(4\) 种:
- \(1~p\):Bitaro 向你第 \(p\) 堆灰尘的坐标。
- \(2~l\):Bitaro 将一个长为 \(l\) 的扫帚放置在 y 轴正半轴上,一端位于原点。然后他水平向右移动扫帚,直到不能移动为止。也就是说,每一堆原来满足 \(x < p - l, y \leq l\) 的灰尘 \((x, y)\) 会被移动到 \((p - l, y)\),注意这个位置可能存在多堆灰尘。
- \(3~l\):Bitaro 将一个长为 \(l\) 的扫帚放置在 x 轴正半轴上,一端位于原点。然后他竖直向上移动扫帚,直到不能移动为止。也就是说,每一堆原来满足 \(x \leq l, y < p - l\) 的灰尘 \((x, y)\) 会被移动到 \((x, p - l)\),注意这个位置可能存在多堆灰尘。
- \(4~x~y\):有一堆新的灰尘出现在 \((x, y)\) 处。如果之前一共有 \(c\) 堆灰尘,那么这堆灰尘就是第 \(c + 1\) 堆灰尘。
你需要编写一个程序,模拟 Bitaro 打扫房间的过程,并回答他的询问。
\(1 \leq p \leq 10^9\),\(1 \leq n \leq 5 \times 10^5\),\(1 \leq q \leq 10^6\)。
- 特殊性质 A:只会发生前三类事件,并且初始时 \(x_i \leq x_{i + 1}, y_i \geq y_{i + 1}\)。
- 特殊性质 B:只会发生前三类事件。
不得不说,特殊性质大大拉低了这道题的难度。
特殊性质 A 怎么做?不难发现经过任意多次操作以后,灰尘的坐标依然满足 \(x_i \leq x_{i + 1}, y_i \geq y_{i + 1}\),因此每次坐标发生变化的灰尘构成一段连续的区间。也就是说,此时我们需要维护一个数据结构,支持二分、区间赋值、单点查询,使用线段树即可。时间复杂度为 \(\Theta(n + q \log n)\)。
特殊性质 B 怎么做?现在横纵坐标不满足单调性了,但我们发现在任意时刻,如果用 \(S\) 来表示此时被移动过至少一次的灰尘构成的集合,那么 \(S\) 中灰尘的横纵坐标仍然满足单调性(具体而言,这些灰尘构成一条从左上到右下、每一步水平向右或竖直向下的折线)。那么我们的做法分成两部分,一部分是求出每一堆灰尘什么时候加入集合 \(S\),另一部分是维护集合 \(S\) 中灰尘的坐标。
对于第一部分,不妨将每次 2、3 操作扫帚的移动范围看作一个矩形,那么我们要求一堆灰尘第一次被移动的时间,就相当于求出覆盖 \((x_i, y_i)\) 这个点的编号最小的矩形。由于矩形的右上角一定在直线 \(x + y = p\) 上,我们不妨先对直线 \(x + y = p\) 上的所有整点求出它对应的矩形第一次出现的时间。那么对于一堆灰尘 \((x_i, y_i)\),覆盖它的矩形的右上角一定构成一段连续的区间,也就是说我们只需要再维护一个支持区间查询最小值的数据结构就可以了,直接上 ST 表即可。注意由于 \(p\) 非常大,我们需要先将能用到的整点离散化一下。对于第二部分,由于此时我们还需要支持动态插入元素,因此将上文的线段树换成平衡树即可。时间复杂度为 \(\Theta((q + n)\log q + q \log n)\)。
现在我们解决了特殊性质,那没有特殊性质怎么办呢?由于我们不希望动态加入灰尘,因此考虑堆每一堆灰尘存在的时间线段树分治,将其放到线段树的 \(\Theta(\log q)\) 个结点上,然后对每个结点都跑一遍特殊性质 B 的做法。时间复杂度为 \(\Theta((q + n)\log (q + n)\log n)\)。
代码
#include <algorithm>
#include <cstdio>
#include <ctime>
#include <iostream>
#include <random>
#include <stack>
#include <vector>
using namespace std;
const int N=(int)1.5e6+3,log_N=22;
mt19937 myrand(time(0));
struct Point{
int x,y;
}a[N],res[N];
struct Operation{
int opt,x;
}op[N];
int n,p,q,len,alls[N],f[N];
vector<int> pos[N]; bool bj[N];
int binary1(int x){
int l=0,r=len;
while(l<r){
int mid=(l+r+1)/2;
(alls[mid]<=x)?(l=mid):(r=mid-1);
}
return l;
}
int binary2(int x){
int l=1,r=len+1;
while(l<r){
int mid=(l+r)/2;
(alls[mid]>=x)?(r=mid):(l=mid+1);
}
return r;
}
namespace ST{
int minn[log_N][N],lgt[N];
void init_ST(int n,int a[N]){
for(int i=2;i<=n;i++) lgt[i]=lgt[i>>1]+1;
for(int i=1;i<=n;i++) minn[0][i]=a[i];
for(int i=1;i<log_N;i++)
for(int j=1;j+(1<<i)-1<=n;j++)
minn[i][j]=min(minn[i-1][j],minn[i-1][j+(1<<i-1)]);
}
int query(int l,int r){
if(l>r) return -1;
int d=lgt[r-l+1];
return min(minn[d][l],minn[d][r-(1<<d)+1]);
}
}
namespace Treap{
struct Node{
int fa,lson,rson,key,chkx,chky;
unsigned val;
}node[N];
int len_node=0,root=0,pos[N];
void clear(){
for(int i=1;i<=len_node;i++)
pos[node[i].key]=0;
len_node=root=0;
}
int make_node(int key){
node[++len_node]={0,0,0,key,0,0,myrand()};
return len_node;
}
void update_x(int u,int x){
if(!u) return ;
node[u].chkx=max(node[u].chkx,x);
a[node[u].key].x=max(a[node[u].key].x,x);
}
void update_y(int u,int x){
if(!u) return ;
node[u].chky=max(node[u].chky,x);
a[node[u].key].y=max(a[node[u].key].y,x);
}
void push_up(int u){
if(node[u].lson) node[node[u].lson].fa=u;
if(node[u].rson) node[node[u].rson].fa=u;
}
void push_down(int u){
if(!u) return ;
if(node[u].chkx>0){
update_x(node[u].lson,node[u].chkx);
update_x(node[u].rson,node[u].chkx);
node[u].chkx=0;
}
if(node[u].chky>0){
update_y(node[u].lson,node[u].chky);
update_y(node[u].rson,node[u].chky);
node[u].chky=0;
}
}
void split_x(int u,int x,int& root1,int& root2){
if(!u){ root1=root2=0; return ; }else push_down(u);
if(a[node[u].key].x<=x)
root1=u,split_x(node[u].rson,x,node[u].rson,root2);
else
root2=u,split_x(node[u].lson,x,root1,node[u].lson);
push_up(u); return ;
}
void split_y(int u,int x,int& root1,int& root2){
if(!u){ root1=root2=0; return ; }else push_down(u);
if(a[node[u].key].y<=x)
root2=u,split_y(node[u].lson,x,root1,node[u].lson);
else
root1=u,split_y(node[u].rson,x,node[u].rson,root2);
push_up(u); return ;
}
int merge(int u,int v){
if(!u||!v) return u+v;
int ans; push_down(u),push_down(v);
if(node[u].val<node[v].val)
ans=u,node[u].rson=merge(node[u].rson,v);
else
ans=v,node[v].lson=merge(u,node[v].lson);
push_up(ans); return ans;
}
void modify_x(int x){
int u1,u2; split_y(root,x,u1,u2);
update_x(u2,p-x),root=merge(u1,u2);
}
void modify_y(int x){
int u1,u2; split_x(root,x,u1,u2);
update_y(u1,p-x),root=merge(u1,u2);
}
void insert(int k){
int u1,u2,u3; pos[k]=make_node(k);
split_x(root,a[k].x,u1,u3),split_y(u1,a[k].y-1,u1,u2);
root=merge(merge(u1,pos[k]),merge(u2,u3));
}
Point query(int k){
if(!pos[k]) return a[k];
int cur=pos[k]; stack<int> stk;
while(cur!=root)
cur=node[cur].fa,stk.push(cur);
while(!stk.empty())
push_down(stk.top()),stk.pop();
return a[k];
}
void traverse(int u){
push_down(u);
if(node[u].lson) traverse(node[u].lson);
if(node[u].rson) traverse(node[u].rson);
}
}
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_N)+3];
vector<int> arr[(1<<log_N)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,int x){
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,Point res[N]){
int i,j,s1,s2; len=0;
for(i=node[u].l;i<=node[u].r;i++)
if(op[i].opt==2) alls[++len]=p-op[i].x;
else if(op[i].opt==3) alls[++len]=op[i].x;
sort(alls+1,alls+len+1);
len=unique(alls+1,alls+len+1)-alls-1;
for(i=node[u].r;i>=node[u].l;i--)
if(op[i].opt==2) f[binary1(p-op[i].x)]=i;
else if(op[i].opt==3) f[binary1(op[i].x)]=i;
ST::init_ST(len,f);
for(i=node[u].l;i<=node[u].r;i++) pos[i].clear();
for(i=0;i<arr[u].size();i++) bj[arr[u][i]]=1;
for(i=0;i<arr[u].size();i++){
s1=arr[u][i],s2=ST::query(binary2(a[s1].x),binary1(p-a[s1].y));
if(s2>0) pos[s2].push_back(s1);
}
Treap::clear();
for(i=node[u].l;i<=node[u].r;i++)
switch(op[i].opt){
case 1: if(bj[op[i].x]) res[i]=Treap::query(op[i].x); break;
case 2:
Treap::modify_x(op[i].x);
for(j=0;j<pos[i].size();j++)
s1=pos[i][j],a[s1].x=p-op[i].x,Treap::insert(s1);
break;
case 3:
Treap::modify_y(op[i].x);
for(j=0;j<pos[i].size();j++)
s1=pos[i][j],a[s1].y=p-op[i].x,Treap::insert(s1);
}
Treap::traverse(Treap::root);
for(i=0;i<arr[u].size();i++) bj[arr[u][i]]=0;
if(node[u].l<node[u].r) solve(u*2,res),solve(u*2+1,res);
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
void write(int x,char ch){
char a[12]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar(ch);
}
int main(){
// freopen("sweep.in","r",stdin);
// freopen("sweep.out","w",stdout);
p=read(),n=read(),q=read();
SGT::build(1,1,q);
for(int i=1;i<=n;i++){
a[i].x=read(),a[i].y=read();
SGT::insert(1,1,q,i);
}
for(int i=1;i<=q;i++){
op[i].opt=read();
if(op[i].opt==4){
n++,a[n].x=read(),a[n].y=read();
SGT::insert(1,i,q,n);
}else op[i].x=read();
}
SGT::solve(1,res);
for(int i=1;i<=q;i++)
if(op[i].opt==1) write(res[i].x,' '),write(res[i].y,'\n');
// fclose(stdin);
// fclose(stdout);
return 0;
}
CF981E Addition on Segments
简要题意
现有一个长度为 \(n\) 的序列,初始时全为 \(0\)。给定 \(q\) 条操作,第 \(i\) 条形如给下标在 \([l, r]\) 中的每个元素加上 \(x\)。
现你要从这 \(q\) 条操作中选出若干条执行,求在 \([1, n]\) 中有哪些值可能成为最终该序列的最大值。
\(1 \leq n, q \leq 10^4\)。
首先我们发现这个最大值实际上是假的,即对于某个 \(x \in [1, n]\),如果我们能够选出一些操作使得位置 \(i\) 上的元素恰为 \(x\),那么删去所有不包含位置 \(i\) 的操作即可使得 \(x\) 变成最大值。也就是说,我们只需要求出哪些值可能在最终序列中出现即可。
如果序列中只有一个元素,那么我们暴力对所有限制跑一遍 0-1 背包即可。现在序列中有多个元素,我们希望对每一个元素都对包含这个元素的限制跑一遍背包,即从左往右枚举每个元素 \(i\),对于一条限制 \(([l, r], x)\),我们希望这条限制在 \(i = l\) 时加入背包,在 \(i = r + 1\) 时从背包中移除。
也就是说,我们需要维护一个可行性 0-1 背包,支持动态插入、删除元素。然而这是可行性背包,不支持删除元素,怎么办呢?
一种方法是把它变成计数背包,即设 \(f(i)\) 表示使用这些元素凑出 \(i\) 的方案数,这样转移方程就从逻辑或变成了加法,就能支持删除操作了。但注意方案数可能很大,因此我们需要使用类似哈希的方式将答案对一个模数取模。时间复杂度为 \(\Theta(nq + n^2)\)。
另一种方法是直接上线段树分治,把删除变成撤销。注意我们不需要真的撤销,只需使用类似可持久化的方式,每次递归下去的时候把整个 DP 数组复制一份。此时仍然是可行性背包,可以用 bitset 将复杂度优化至 \(\Theta(\frac{nq \log n}{\omega})\)。
代码 1(可删除背包)
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
const int N=10003,mod=998244353;
int n,f[N]; bool res[N];
vector<int> ins[N],rmv[N];
int main(){
// freopen("segment.in","r",stdin);
// freopen("segment.out","w",stdout);
int i,j,k,q,l,r,x,cnt=0;
scanf("%d%d",&n,&q);
for(i=1;i<=q;i++){
scanf("%d%d%d",&l,&r,&x);
ins[l ].push_back(x);
rmv[r+1].push_back(x);
}
for(f[0]=1,i=1;i<=n;i++){
for(j=0;j<ins[i].size();j++)
for(k=n;k>=ins[i][j];k--)
f[k]=(f[k]+f[k-ins[i][j]])%mod;
for(j=0;j<rmv[i].size();j++)
for(k=rmv[i][j];k<=n;k++)
f[k]=(f[k]-f[k-rmv[i][j]]+mod)%mod;
for(j=1;j<=n;j++) res[j]|=(f[j]>0);
}
for(i=1;i<=n;i++) cnt+=res[i];
printf("%d\n",cnt);
for(i=1;i<=n;i++)
if(res[i]) printf("%d ",i);
// fclose(stdin);
// fclose(stdout);
return 0;
}
代码 2(线段树分治)
#include <bitset>
#include <cstdio>
#include <vector>
using namespace std;
const int N=10003,log_N=15;
int n,q; bitset<N> res,f;
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_N)+3];
vector<int> arr[(1<<log_N)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,int x){
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,bitset<N> f,bitset<N>& res){
for(int i=0;i<arr[u].size();i++) f|=f<<arr[u][i];
if(node[u].l==node[u].r) res|=f;
else solve(u*2,f,res),solve(u*2+1,f,res);
}
}
int main(){
// freopen("segment.in","r",stdin);
// freopen("segment.out","w",stdout);
int i,l,r,x,cnt=0;
scanf("%d%d",&n,&q);
SGT::build(1,1,n);
for(i=1;i<=q;i++){
scanf("%d%d%d",&l,&r,&x);
SGT::insert(1,l,r,x);
}
f.set(0),SGT::solve(1,f,res);
for(i=1;i<=n;i++) cnt+=res[i];
printf("%d\n",cnt);
for(i=1;i<=n;i++)
if(res[i]) printf("%d ",i);
// fclose(stdin);
// fclose(stdout);
return 0;
}
CF601E A Museum Robbery
简要题意
现有一集合 \(S\),初始时 \(S\) 内有 \(n\) 件物品,第 \(i\) 件的价值为 \(v_i\),质量为 \(w_i\)。
再给定正整数 \(m\),对于 \(k \in [0, m]\),定义 \(f(S, k)\) 表示从集合 \(S\) 中选出若干件物品,使它们的总质量不超过 \(k\),则它们的价值之和的最大值是多少。
\(q\) 次操作,每次操作为以下三种类型之一:
- \(1~v~w\):向集合 \(S\) 中添加一个价值为 \(v\)、质量为 \(w\) 的新物品。若之前集合 \(S\) 中总共出现过 \(c\) 件物品(被删掉的也算),则本次添加的物品编号为 \(c + 1\)。
- \(2~x\):从集合 \(S\) 中删掉编号为 \(x\) 的物品。
- \(3\):你需要求出以下式子的值,其中 \(M = 10^7 + 19, P = 10^9 + 7\):
\(1 \leq n \leq 5000\),\(1 \leq m, w, w_i \leq 1000\),\(1 \leq q \leq 3 \times 10^4\),\(1 \leq v, v_i \leq 10^6\),保证操作 1 不超过 \(10^4\) 次。
我们需要维护一个最优性 0-1 背包,支持动态加入、删除物品。
0-1 背包不支持删除,怎么办?直接上线段树分治,把删除变成撤销,然后使用类似可持久化的方式维护 0-1 背包即可。时间复杂度为 \(\Theta((n + q)m \log q)\)。
代码
#include <cstdio>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
const int N=15003,Q=30003,log_Q=16,M=1003;
const int Hmul=(int)1e7+19,Hmod=(int)1e9+7;
struct Object{
int a,b;
}a[N];
int n,m,q,mp[N],res[Q];
vector<int> qu;
struct Backpack{
int f[M];
Backpack(){
for(int i=0;i<=m;i++) f[i]=0;
}
void insert(Object x){
for(int i=m;i>=x.a;i--)
f[i]=max(f[i],f[i-x.a]+x.b);
}
int query(){
int ans=0,i;
for(i=m;i>0;i--) ans=(1ll*ans*Hmul%Hmod+f[i])%Hmod;
return ans;
}
};
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_Q)+3];
vector<Object> arr[(1<<log_Q)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,const Object x){
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,Backpack dp,int res[N]){
for(int i=0;i<arr[u].size();i++) dp.insert(arr[u][i]);
if(node[u].l==node[u].r) res[node[u].l]=dp.query();
else solve(u*2,dp,res),solve(u*2+1,dp,res);
}
}
int main(){
// freopen("robbery.in","r",stdin);
// freopen("robbery.out","w",stdout);
int i,opt,x;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)
scanf("%d%d",&a[i].b,&a[i].a),mp[i]=0;
scanf("%d",&q);
SGT::build(1,0,q);
for(i=1;i<=q;i++){
scanf("%d",&opt);
switch(opt){
case 1: mp[++n]=i; scanf("%d%d",&a[n].b,&a[n].a); break;
case 2: scanf("%d",&x); SGT::insert(1,mp[x],i-1,a[x]); mp[x]=-1; break;
case 3: qu.push_back(i);
}
}
for(i=1;i<=n;i++)
if(mp[i]>=0) SGT::insert(1,mp[i],q,a[i]);
SGT::solve(1,Backpack(),res);
for(i=0;i<qu.size();i++)
printf("%d\n",res[qu[i]]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
CF678F Lena and Queries
简要题意
你需要维护一个集合 \(S\),初始为空。\(q\) 次操作,每次操作为以下三种类型之一:
- \(1~x~y\):将一个二元组 \((x, y)\) 加入集合 \(S\)。
- \(2~k\):从集合 \(S\) 中删去第 \(k\) 次操作加入的二元组,保证第 \(k\) 次操作为操作 1 且还未被删去。
- \(3~k\):对集合 \(S\) 中的所有二元组 \((x, y)\),求 \(kx + y\) 的最大值,或报告 \(S = \emptyset\)。
\(1 \leq q \leq 3 \times 10^5\),\(x, y \in [-10^9, 10^9]\),操作 3 中的 \(k \in [-10^9, 10^9]\)。
将每个二元组视作二维平面上的点,那么查询相当于求斜率为 \(-k\) 且经过至少一个点的直线中,截距最大是多少。显然这个直线与 \(S\) 中点构成的上凸包相切,因此只要求出了 \(S\) 中点的凸包,我们就能在凸壳上二分找到斜率为 \(-k\) 的直线与上凸壳的切点,进而求出截距的最大值。也就是说,我们需要动态维护 \(S\) 中点的上凸包,支持动态加点、删点。
支持动态加点很好做,直接用平衡树维护凸包即可。但问题在于凸包难以支持删点操作,因此考虑用线段树分治把删除变成撤销,将每个点出现的时间段挂到线段树的 \(\Theta(\log q)\) 个结点上,然后遍历一遍线段树,用可持久化平衡树维护凸包即可。时间复杂度为 \(\Theta(q \log^2 q)\)。
然而平衡树常数较大且比较难写,怎么办?考虑将对一个询问的贡献拆到线段树的 \(\Theta(\log n)\) 层结点上,对线段树上的每个结点,我们只需要考虑“这个结点上挂着的所有点”对“这个结点代表的时间段包含的所有询问”的贡献,容易发现这样拆出的贡献是不重不漏的。这样做的好处在于我们不仅不需要支持撤销操作,连加点的顺序也变得自由了许多。于是我们就可以将点和询问分别按横坐标和斜率排序,然后直接用队列维护凸包即可。
时间复杂度为 \(\Theta(q \log^2 q)\),精细实现即可做到 \(\Theta(q \log q)\)。注意这种方法是有局限性的,只使用于这种每个点的贡献相对比较独立的情况,比如如果我们要动态维护图的连通性就不能这么拆贡献,只能用可撤销并查集硬做。
代码
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
const int N=300003,log_N=20;
const long long NIN=-4e18,PIN=4e18;
struct Point{
long long x,y;
bool operator<(const Point u)const{
return (x!=u.x)?(x<u.x):(y<u.y);
}
}a[N];
struct Question{
long long k; int p;
Question(long long _k,int _p){
k=_k,p=_p;
}
bool operator<(const Question a)const{
return k>a.k;
}
};
int n,q; bool bj[N];
long long res[N];
double get_k(Point u,Point v){
if(u.x==v.x) return (u.y<v.y)?PIN:NIN;
return (double)(v.y-u.y)/(v.x-u.x);
}
namespace Hull{
int hh,tt; Point a[N];
void clear(){ hh=1,tt=0; }
void push(Point u){
while(hh<tt&&get_k(a[tt-1],a[tt])<=get_k(a[tt],u)) tt--;
a[++tt]=u;
}
long long query(long long k){
if(hh>tt) return NIN;
while(hh<tt&&get_k(a[hh],a[hh+1])>=k) hh++;
return a[hh].y-k*a[hh].x;
}
}
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_N)+3];
vector<Point> op[(1<<log_N)+3];
vector<Question> qu[(1<<log_N)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert_op(int u,int l,int r,const Point x){
if(node[u].l>=l&&node[u].r<=r)
op[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert_op(u*2 ,l,r,x);
if(r> mid) insert_op(u*2+1,l,r,x);
}
}
void insert_qu(int u,int k,const Question x){
qu[u].push_back(x);
if(node[u].l<node[u].r){
int mid=(node[u].l+node[u].r)/2;
insert_qu(u*2+(k>mid),k,x);
}
}
void solve(int u,long long res[N]){
int i; Hull::clear();
sort(op[u].begin(),op[u].end());
sort(qu[u].begin(),qu[u].end());
for(i=0;i<op[u].size();i++) Hull::push(op[u][i]);
for(i=0;i<qu[u].size();i++)
res[qu[u][i].p]=max(res[qu[u][i].p],Hull::query(qu[u][i].k));
if(node[u].l<node[u].r) solve(u*2,res),solve(u*2+1,res);
}
}
int read(){
char ch; int x=0; bool bj=0;
do ch=getchar();
while(ch!='-'&&(ch<'0'||ch>'9'));
if(ch=='-') bj=1,ch=getchar();
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return bj?-x:x;
}
void write(long long x){
if(x<0) putchar('-'),x=-x;
char a[24]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar('\n');
}
int main(){
// freopen("process.in","r",stdin);
// freopen("process.out","w",stdout);
int i,s1,s2;
SGT::build(1,1,s1=read());
for(i=1;i<=s1;i++)
switch(read()){
case 1: a[i].x=read(),a[i].y=read(),bj[i]=1; break;
case 2: bj[s2=read()]=0,SGT::insert_op(1,s2,i-1,a[s2]); break;
case 3: SGT::insert_qu(1,i,Question(-read(),++q));
}
for(i=1;i<=s1;i++)
if(bj[i]) SGT::insert_op(1,i,s1,a[i]);
for(i=1;i<=q;i++) res[i]=NIN;
SGT::solve(1,res);
for(i=1;i<=q;i++)
if(res[i]>NIN/2) write(res[i]);
else puts("EMPTY SET");
// fclose(stdin);
// fclose(stdout);
return 0;
}
洛谷 P4121 [WC2005] 双面棋盘
简要题意
有一个 \(n \times n\) 的棋盘,初始时第 \(i\) 行第 \(j\) 列的方格有颜色 \(a_{i, j}\)。保证 \(0 \leq a_{i, j} \leq 1\)。
\(m\) 次操作,每次操作给定 \(x, y\),表示将第 \(i\) 行第 \(j\) 列的方格的颜色取反。你需要在每次操作后回答颜色为 \(1\) 的方格构成的连通块与颜色为 \(0\) 的方格构成的连通块分别有多少个。
\(1 \leq n \leq 200\),\(1 \leq m \leq 10^4\)。
线段树分治板子题。考虑将操作离线下来,将每个方格在每种颜色的状态下存在的时间插入线段树,然后线段树分治,用可撤销并查集维护连通性即可。时间复杂度为 \(\Theta((n^2 + m) \log n \log m)\)。
当然由于评测机性能的提升,每次修改重新暴力维护连通性也是可以通过的。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int dx[4]={0,1,0,-1};
const int dy[4]={1,0,-1,0};
const int N=203,M=10003,log_M=15;
struct Point{
int x,y,c;
Point(){ x=y=c=0; }
Point(int _x,int _y,int _c){
x=_x,y=_y,c=_c;
}
};
int n,m,cnt[M][2],a[N][N],tim[N][N],res[M][2];
inline int ge(int x,int y){
return (x-1)*n+y;
}
struct DSU{
int fa[N*N],rnk[N*N],len,his[N*N*2],cnt;
void init_DSU(int n){
len=0,cnt=n;
for(int i=1;i<=n;i++)
fa[i]=i,rnk[i]=1;
}
int find(int u){
while(fa[u]!=u) u=fa[u];
return u;
}
void merge(int u,int v){
u=find(u),v=find(v);
if(rnk[u]<rnk[v]) swap(u,v);
if(u==v) his[++len]=-1;
else rnk[u]+=rnk[v],his[++len]=v,fa[v]=u,cnt--;
}
void rollback(){
int u=his[len--];
if(u>0) rnk[fa[u]]-=rnk[u],fa[u]=u,cnt++;
}
}dsu[2];
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_M)+3];
vector<Point> arr[(1<<log_M)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,Point p){
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(p);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,p);
if(r> mid) insert(u*2+1,l,r,p);
}
}
void solve(int u,int res[M][2]){
int i,j; Point s1,s2;
for(i=0;i<arr[u].size();i++){
s1=arr[u][i],a[s1.x][s1.y]=s1.c;
for(j=0;j<4;j++){
s2.x=s1.x+dx[j],s2.y=s1.y+dy[j];
if(s2.x<1||s2.y<1||s2.x>n||s2.y>n) continue;
if(a[s2.x][s2.y]==s1.c)
dsu[s1.c].merge(ge(s1.x,s1.y),ge(s2.x,s2.y));
}
}
if(node[u].l==node[u].r){
res[node[u].l][0]=dsu[0].cnt-cnt[node[u].l][1];
res[node[u].l][1]=dsu[1].cnt-cnt[node[u].l][0];
}else solve(u*2,res),solve(u*2+1,res);
for(i=arr[u].size()-1;i>=0;i--){
s1=arr[u][i],a[s1.x][s1.y]=-1;
for(j=0;j<4;j++){
s2.x=s1.x+dx[j],s2.y=s1.y+dy[j];
if(s2.x<1||s2.y<1||s2.x>n||s2.y>n) continue;
if(a[s2.x][s2.y]==s1.c) dsu[s1.c].rollback();
}
}
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
int main(){
// freopen("chessboard.in","r",stdin);
// freopen("chessboard.out","w",stdout);
int i,j,x,y; n=read();
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
a[i][j]=read(),cnt[0][a[i][j]]++;
SGT::build(1,0,m=read());
for(i=1;i<=m;i++){
x=read(),y=read();
SGT::insert(1,tim[x][y],i-1,Point(x,y,a[x][y]));
cnt[i][0]=cnt[i-1][0],cnt[i][1]=cnt[i-1][1];
cnt[i][a[x][y]]--,cnt[i][!a[x][y]]++;
a[x][y]^=1,tim[x][y]=i;
}
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
SGT::insert(1,tim[i][j],m,Point(i,j,a[i][j]));
dsu[0].init_DSU(n*n),dsu[1].init_DSU(n*n);
memset(a,-1,sizeof a),SGT::solve(1,res);
for(i=1;i<=m;i++) printf("%d %d\n",res[i][1],res[i][0]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
ABC308Ex Make Q
简要题意
给定一张 \(n\) 个点、\(m\) 条边的简单无向图,边有边权。定义一个非空边子集的权值为其包含的所有边的边权之和。
我们称一个非空边子集 \(S\) 是合法的当且仅当其中恰好 \(|S| - 1\) 条边构成一个简单环,且剩下的那条边连接一个该环上的点和一个不在该环上的点。求合法边子集的最小权值,如果不存在这样的边子集输出 \(-1\)。
\(4 \leq n \leq 300\),\(4 \leq m \leq \frac{n(n - 1)}{2}\)。
下文中默认 \(m = \Theta(n^2)\)。记 \(w(x, y)\) 表示连接 \(x, y\) 的无向边的权值。
首先考虑弱化一下条件,我们约定如果剩下的那条边连接的两个点都在环上,那么该边子集也是合法的。考虑这条环外边的两个端点会把环分成两条链,我们取其中较长的一条链。显然该链的长度至少为 \(2\),那么我们只保留该链最边上的一条边,将该链上的其他边全部删掉。考虑这样操作得到的图中,较短的链与环外边构成了一个新的环,而我们刚才保留的那条边连接了一个环上的点和一个环外的点,因此这个图即便在弱化条件之前也是合法的。这告诉我们对于任意一个通过弱化条件得到的边子集,我们都可以找到另一个权值严格更小边子集,也就是说,弱化条件后答案不会发生任何变化。
那么我们有了一个自然的想法:枚举环外边连接的环上的点 \(u\),然后枚举环外边连接的另一个点 \(v\),这样我们就确定了环外边 \((u, v)\),然后将这条边删去求包含点 \(u\) 的最小环即可。但由于这是稠密图,求最短路的时间复杂度至少为 \(\Theta(n^2)\),而我们外面还套了 \(\Theta(n^2)\) 的枚举量,因此时间复杂度至少为 \(\Theta(n^4)\),无法通过。
但我们发现对于每个 \(u\),它连接的环外边至多只有三种可能。考虑在不删边的情况下求出包含点 \(u\) 的最小环,其中与 \(u\) 相连的两个点分别为 \(x, y\),那么对于任意不等于 \(x, y\) 的点 \(v\),环外边 \((u, v)\) 对应的最小环一定是我们刚才求出的、全局包含点 \(u\) 的最小环。因此对于 \(v \neq x, v \neq y\),只有当 \(w(u, v)\) 取最小值的时候我们枚举的环外边才是有意义的。因此对于一个固定的 \(u\),我们将环外边的枚举量降至了 \(\Theta(1)\),此时复杂度的下界降至了 \(\Theta(n^3)\),理论上有希望通过。
接下来我们只需要考虑怎么求包含点 \(u\) 的最小环即可。一种很自然的思路是首先用 Floyd 预处理出任意 \(i, j\) 间不经过点 \(u\)的最短路径 \(\mathrm{dis}(i, j)\),然后枚举环上与 \(u\) 相邻的点 \(x, y\),那么 \(w(u, x) + w(u, y) + \mathrm{dis}(i, j)\) 的最小值即为最小环的权值。考虑 Floyd 算法的实质是依次将 \(1, 2, \cdots, n\) 加入中间点集合,然后更新 DP 数组,这里我们要强制中间点集合不包含 \(u\),也就是说我们需要一个支持删除操作的 Floyd 算法。但由于求最短路的 Floyd 不支持删除,因此我们直接上线段树分治,将删除变成撤销即可。总时间复杂度为 \(\Theta(n^3 \log n)\)。
但其实求包含点 \(u\) 的最小环是可以做到 \(\Theta(n^2)\) 的:首先以 \(u\) 为起点跑一遍单源最短路,然后建出最短路树。由调整法可知,包含点 \(u\) 的最小环一定有恰好一条边不在最短路树上,因此我们枚举这条边 \((i, j)\),那么 \(w(i, j) + \mathrm{dis}(i) + \mathrm{dis}(j)\) 的最小值即为答案。总时间复杂度为 \(\Theta(n^3)\)。
代码 1(线段树分治 + Floyd)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N=303,PIN=1061109567;
int n,m,ans=PIN;
struct Floyd{
int dis[N][N];
void init_Floyd(){
memset(dis,0x3f,sizeof dis);
for(int i=1;i<=n;i++) dis[i][i]=0;
}
void update(int u){
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][u]+dis[u][j]);
}
}e;
void solve(int l,int r,const Floyd& f){
if(l==r){
int i,j,s1=PIN,s2=PIN,s3=PIN,s4;
for(i=1;i<=n;i++){
if(i==l) continue;
if(e.dis[l][i]<=s1) s3=s2,s2=s1,s1=e.dis[l][i];
else if(e.dis[l][i]<=s2) s3=s2,s2=e.dis[l][i];
else s3=min(s3,e.dis[l][i]);
}
for(i=1;i<=n;i++)
for(j=i+1;j<=n;j++){
if(i==l||j==l||f.dis[i][j]==PIN) continue;
if(e.dis[l][i]==PIN||e.dis[l][j]==PIN) continue;
if(e.dis[l][i]==s1&&e.dis[l][j]==s2) s4=s3;
else if(e.dis[l][i]==s2&&e.dis[l][j]==s1) s4=s3;
else s4=(e.dis[l][i]==s1||e.dis[l][j]==s1)?s2:s1;
ans=min(ans,e.dis[l][i]+e.dis[l][j]+f.dis[i][j]+s4);
}
}else{
int mid=(l+r)/2,i; Floyd sl=f,sr=f;
for(i=r;i> mid;i--) sl.update(i);
for(i=l;i<=mid;i++) sr.update(i);
solve(l,mid,sl),solve(mid+1,r,sr);
}
}
int main(){
// freopen("qmake.in","r",stdin);
// freopen("qmake.out","w",stdout);
int i,x,y,z;
scanf("%d%d",&n,&m),e.init_Floyd();
for(i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
e.dis[x][y]=e.dis[y][x]=z;
}
solve(1,n,e);
printf("%d",(ans>PIN/2)?-1:ans);
// fclose(stdin);
// fclose(stdout);
return 0;
}
代码 2(Dijkstra)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <stack>
using namespace std;
const int N=303,PIN=1061109567;
int n,m,e[N][N],dis[N],pre[N],bel[N];
stack<int> stk;
void Dijkstra(int S,int ban=0){
int i,j,s1,s2; bool bj[N]={0};
memset(dis,0x3f,sizeof dis);
memset(pre,-1,sizeof pre);
dis[S]=0,bj[ban]=1;
while(true){
for(s1=PIN,s2=-1,i=1;i<=n;i++)
if(!bj[i]&&dis[i]<s1) s1=dis[i],s2=i;
if(s2==-1) break; else bj[s2]=1;
for(i=1;i<=n;i++)
if(!bj[i]&&s1+e[s2][i]<dis[i])
dis[i]=s1+e[s2][i],pre[i]=s2;
}
for(i=1;i<=n;i++) bel[i]=(pre[i]==S)?i:-1;
for(i=1;i<=n;i++){
if(pre[i]==-1) continue;
for(j=i;bel[j]==-1;j=pre[j]) stk.push(j);
while(!stk.empty()) bel[stk.top()]=bel[j],stk.pop();
}
}
int main(){
// freopen("qmake.in","r",stdin);
// freopen("qmake.out","w",stdout);
int i,j,k,x,y,z,ans=PIN,s1,s2;
scanf("%d%d",&n,&m);
memset(e,0x3f,sizeof e);
for(i=1;i<=n;i++) e[i][i]=0;
for(i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
e[x][y]=e[y][x]=z;
}
for(i=1;i<=n;i++){
Dijkstra(i),s1=PIN,x=y=-1;
for(j=1;j<=n;j++){
if(pre[j]==-1) continue;
for(k=j+1;k<=n;k++){
if(pre[k]==-1||bel[k]==bel[j]) continue;
if(dis[j]+dis[k]+e[j][k]<s1)
s1=dis[j]+dis[k]+e[j][k],x=bel[j],y=bel[k];
}
}
for(j=1;j<=n;j++)
if(j!=i&&pre[j]!=i&&dis[j]+e[i][j]<s1)
s1=dis[j]+e[i][j],x=j,y=bel[j];
if(x==-1) continue;
for(s2=PIN,j=1;j<=n;j++)
if(j!=x&&j!=y&&j!=i) s2=min(s2,e[i][j]);
ans=min(ans,s1+s2);
for(Dijkstra(i,x),s1=PIN,j=1;j<=n;j++){
if(pre[j]==-1) continue;
for(k=j+1;k<=n;k++)
if(pre[k]>0&&bel[k]!=bel[j])
s1=min(s1,dis[j]+dis[k]+e[j][k]);
}
for(j=1;j<=n;j++)
if(j!=i&&j!=x&&pre[j]!=i)
s1=min(s1,dis[j]+e[i][j]);
ans=min(ans,s1+e[i][x]);
for(Dijkstra(i,y),s1=PIN,j=1;j<=n;j++){
if(pre[j]==-1) continue;
for(k=j+1;k<=n;k++)
if(pre[k]>0&&bel[k]!=bel[j])
s1=min(s1,dis[j]+dis[k]+e[j][k]);
}
for(j=1;j<=n;j++)
if(j!=i&&j!=y&&pre[j]!=i)
s1=min(s1,dis[j]+e[i][j]);
ans=min(ans,s1+e[i][y]);
}
printf("%d",(ans>PIN/2)?-1:ans);
// fclose(stdin);
// fclose(stdout);
return 0;
}
CF603E Pastoral Oddities
简要题意
现有一张 \(n\) 个点的无向图。称一个边集是合法的,当且仅当它若在无向图上只保留该边集中的边,那么无向图上每个点的度数均为奇数。
无向图初始时没有边。现依次加入 \(m\) 条带权的边,每次加入后询问是否存在合法的边子集。如果存在,你需要输出合法边子集中最大边权的最小值;如果不存在,输出 \(-1\)。
\(2 \leq n \leq 10^5\),\(1 \leq m \leq 3 \times 10^5\)。
考虑存在合法边子集的充要条件是什么。对于一个合法的边子集,考虑只保留其中的边形成的图,显然我们需要将每个连通块分开考虑。对于每个连通块,由于点的总度数是偶数,而每个点的度数都是奇数,因此点的数量一定为偶数。而我们知道若两个点在该边子集中联通,则它们在原来的边集中也一定联通,也就是说,存在合法边子集的必要条件为图中每个连通块都有偶数个点。
事实上这个条件也是充分的。考虑按如下方法构造一个边子集 \(S\):对于每个连通块,我们找出任意一颗生成树并任意钦定一个根,然后对于每个非根节点,如果它的子树中有奇数个结点,我们就把它与父亲的连边加入集合 \(S\) 中,否则不加入。由于每个连通块都有偶数个结点,因此如果只保留 \(S\) 中的边,那么每个结点的度数均为 \(1\)。综上,这个条件是存在合法边子集的充要条件。
显然这个条件可以用并查集轻松维护,因此我们得到了一个暴力做法:对于每次询问,我们先将所有边按边权从小到大排序,然后依次加入并查集直到每个连通块的大小均为偶数。如果将现有的边加完后仍然存在大小为奇数的连通块,那么答案为 \(-1\),否则答案为第一次满足条件的时候加的边的边权。这个做法的时间复杂度为 \(\Theta(m^2 \cdot \alpha(n))\),显然无法通过。
那么怎么优化呢?如果将不存在合法边子集视为答案为 \(+\infty\),那么询问的答案是单调不增的,考虑本题不存在删边操作,也就是说这次询问的合法边子集到下一次询问仍然是合法的边子集。基于答案的单调性,我们有两种做法:一种是整体二分,一种是线段树分治。
方法 1:整体二分
考虑单次询问怎么做。刚才我们是直接递推的,事实上我们也可以这么做:考虑二分答案 \(\mathrm{mid}\),判断是否存在所有边权都不超过 \(\mathrm{mid}\) 的合法边子集,具体方法是向并查集中加入现有的所有边权不超过 \(\mathrm{mid}\) 的边,然后判断是否满足所有连通块的大小均为偶数。这么做虽然使单次询问的复杂度劣化成 \(\Theta(m \log m \cdot \alpha(n))\),但其拓展性比上面的递推做法更强。
将所有边按边权从小到大排序,那么每条边拥有两个属性:下标 \(i\) 表示它的编号(即边权的排名),\(p\) 表示它在第几次操作中加入边集。再预处理出 \(a_i\) 表示第 \(i\) 次操作中加入的边的编号。那么第 \(i\) 次询问的二分的目的就变成了:找到最小的编号 \(k\),使得只加入编号在 \([1, k]\) 中且 \(p \leq i\) 的边,得到的图满足条件。我们称这条二分出的边为第 \(i\) 次询问的答案边。
考虑对所有询问进行整体二分,设函数 solve(vl, vr, pl, pr) 表示求出询问 \([\mathrm{pl}, \mathrm{pr}]\) 的答案边,且已知它们的编号都在 \([\mathrm{vl}, \mathrm{vr}]\) 中。那么在一次 \(\mathrm{solve}\) 函数中我们需要对 \(\mathrm{mid} = \frac{\mathrm{vl} + \mathrm{vr}}{2}\),求出哪些询问的答案边在 \([\mathrm{vl}, \mathrm{mid}]\) 中,哪些询问的答案边在 \([\mathrm{mid} + 1, \mathrm{vr}]\) 中,然后再向两边递归下去。
考虑使用并查集维护连通块的信息,那么对于询问 \(i\),我们需要向并查集中加入编号在 \([1, \mathrm{mid}]\) 中,且 \(p \in [1, i]\) 的边。但是如果每次二分都暴力加入这些边复杂度是会爆炸的。我们考虑什么操作的复杂度是能接受的:
- 向并查集中加入编号在 \([\mathrm{vl}, \mathrm{vr}]\) 中的边,或其子集。
- 向并查集中加入满足 \(p \in [\mathrm{pl}, \mathrm{pr}]\) 的边,或其子集。
由于每次二分都会将 \([\mathrm{vl}, \mathrm{vr}]\) 和 \([\mathrm{pl}, \mathrm{pr}]\) 分别划分成两部分,且总共只会进行 \(Theta(\log m)\) 层二分,因此有且仅有上面两种操作的复杂度可以接受。那么在每次二分前我们需要先将所有编号在 \([1, \mathrm{vl})\) 且 \(p \in [1, \mathrm{pl}]\) 的边加入并查集,这样才能保证复杂度正确,而这个操作可以在二分中递归地完成。那么每次二分后我们还需要将这次二分加的边撤销掉,将并查集换成可撤销并查集即可。
时间复杂度为 \(\Theta(m \log m \log n)\),可以通过。
代码 1(整体二分)
#include <algorithm>
#include <cstdio>
#include <iostream>
using namespace std;
const int N=100003,M=300003;
const int PIN=1061109567;
struct Edge{
int u,v,w,p;
}edge[M];
int n,m,a[M],res[M];
namespace DSU{
int fa[N],rnk[N],len,his[M],cnt;
void init_DSU(int n){
len=0,cnt=n;
for(int i=1;i<=n;i++)
fa[i]=i,rnk[i]=1;
}
int find(int u){
while(fa[u]!=u) u=fa[u];
return u;
}
void merge(int u,int v){
u=find(u),v=find(v);
if(rnk[u]<rnk[v]) swap(u,v);
if(u==v) his[++len]=-1;
else{
cnt-=rnk[u]&1,cnt-=rnk[v]&1;
rnk[u]+=rnk[v],cnt+=rnk[u]&1;
fa[v]=u,his[++len]=v;
}
}
void rollback(){
int u=his[len--]; if(u==-1) return ;
cnt-=rnk[fa[u]]&1,rnk[fa[u]]-=rnk[u];
cnt+=rnk[fa[u]]&1,cnt+=rnk[u]&1,fa[u]=u;
}
bool query(){ return !cnt; }
}
void solve(int l,int r,int pl,int pr){
if(pl>pr) return ;
if(edge[l].w==edge[r].w)
for(int i=pl;i<=pr;i++) res[i]=edge[l].w;
else{
int mid=(l+r)/2,s1,i;
for(i=l;i<=mid;i++)
if(edge[i].p<pl) DSU::merge(edge[i].u,edge[i].v);
for(i=pl;i<=pr;i++){
if(a[i]>mid) continue;
DSU::merge(edge[a[i]].u,edge[a[i]].v);
if(DSU::query()) break;
}
for(s1=i-1,i=min(i,pr);i>=pl;i--)
if(a[i]<=mid) DSU::rollback();
solve(mid+1,r,pl,s1);
for(i=mid;i>=l;i--)
if(edge[i].p<pl) DSU::rollback();
for(i=pl;i<=s1;i++){
if(a[i]>=l) continue;
DSU::merge(edge[a[i]].u,edge[a[i]].v);
}
solve(l,mid,s1+1,pr);
for(i=s1;i>=pl;i--)
if(a[i]<l) DSU::rollback();
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
char a[12]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar('\n');
}
int main(){
// freopen("oddity.in","r",stdin);
// freopen("oddity.out","w",stdout);
int i; n=read(),m=read();
for(i=1;i<=m;i++)
edge[i].u=read(),edge[i].v=read(),edge[i].w=read(),edge[i].p=i;
sort(edge+1,edge+m+1,[&](const Edge& a,const Edge& b){
return a.w<b.w;
});
edge[m+1].w=PIN;
for(i=1;i<=m;i++) a[edge[i].p]=i;
DSU::init_DSU(n),solve(1,m+1,1,m);
for(i=1;i<=m;i++)
write((res[i]==PIN)?-1:res[i]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
方法 2:线段树分治
我们将递推换成二分,然后在整体二分的过程中进行一通非常复杂的操作,其根本原因在于并查集不支持删边。考虑如果存在支持删边的并查集,再结合答案的单调性,那么做法就非常简单了:我们在线回答所有询问,每次先加入这次操作所加的边,然后不断删去图中边权最大的边,直到出现大小为奇数的连通块,那么最后一次删掉的边的边权即为答案。
由上面的想法可知,如果我们实时维护边权小于等于答案的边构成的集合,那么每条边的出现时间构成一段区间。因此我们考虑线段树分治,将每条边按出现时间扔到线段树上,那么我们就能把删除操作变成撤销操作,就可以维护了。但现在的问题在于对于每条边出现的时间区间,我们只知道其左端点,不知道其右端点。怎么办呢?
解决办法其实非常简单——我们在分治的过程中动态地向线段树上加边。具体地,我们先将所有边按边权升序排序,然后在线段树上从右向左分治。每次分治到叶结点的时候,设叶结点表示操作 \(l\),我们将还没加入过图且 \(p \leq l\) 的边按边权一次加入并查集,直到所有连通块的大小都为偶数的时候停止。那么对于刚才加入并查集的边,我们就确定了它存在时间的右端点,此时将它插到线段树的前面部分即可。对于还没加入的边,如果 \(p > l\) 就说明它没有用了,否则就说明我们还没有遍历到它的右端点,继续向前分治即可。综上,我们可以在分治的过程中动态确定每条线段存在时间的右端点,因此动态向线段树上加边即可。
时间复杂度为 \(\Theta(m \log m \log n)\)。注意这个“动态分治”的思想还是非常重要的。
代码
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
const int N=100003,M=300003;
const int PIN=1061109567;
struct Edge{
int u,v,w,p;
}edge[M];
int n,m,res[M];
namespace DSU{
int fa[N],rnk[N],len,his[M],cnt;
void init_DSU(int n){
len=0,cnt=n;
for(int i=1;i<=n;i++)
fa[i]=i,rnk[i]=1;
}
int find(int u){
while(fa[u]!=u) u=fa[u];
return u;
}
void merge(int u,int v){
u=find(u),v=find(v);
if(rnk[u]<rnk[v]) swap(u,v);
if(u==v) his[++len]=-1;
else{
cnt-=rnk[u]&1,cnt-=rnk[v]&1;
rnk[u]+=rnk[v],cnt+=rnk[u]&1;
his[++len]=v,fa[v]=u;
}
}
void rollback(){
int u=his[len--]; if(u==-1) return ;
cnt-=rnk[fa[u]]&1,rnk[fa[u]]-=rnk[u];
cnt+=rnk[fa[u]]&1,cnt+=rnk[u]&1,fa[u]=u;
}
bool query(){ return !cnt; }
}
namespace SGT{
struct Node{
int l,r;
}node[M*4];
vector<int> arr[M*4];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,int x){
if(l>r) return ;
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,int& cur,int res[M]){
int i,s1;
for(i=0;i<arr[u].size();i++)
s1=arr[u][i],DSU::merge(edge[s1].u,edge[s1].v);
if(node[u].l==node[u].r){
for(s1=cur;cur<=m;cur++){
if(edge[cur].p>node[u].l) continue;
DSU::merge(edge[cur].u,edge[cur].v);
if(DSU::query()) break;
insert(1,edge[cur].p,node[u].l-1,cur);
}
res[node[u].l]=edge[cur].w;
for(i=min(cur,m);i>=s1;i--)
if(edge[i].p<=node[u].l) DSU::rollback();
}else solve(u*2+1,cur,res),solve(u*2,cur,res);
for(i=0;i<arr[u].size();i++) DSU::rollback();
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
char a[12]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar('\n');
}
int main(){
// freopen("oddity.in","r",stdin);
// freopen("oddity.out","w",stdout);
int i,s1; n=read(),m=read();
for(i=1;i<=m;i++)
edge[i].u=read(),edge[i].v=read(),edge[i].w=read(),edge[i].p=i;
sort(edge+1,edge+m+1,[&](const Edge& a,const Edge& b){
return a.w<b.w;
});
DSU::init_DSU(n),edge[m+1].w=PIN;
SGT::build(1,1,m),SGT::solve(1,s1=0,res);
for(i=1;i<=m;i++) write((res[i]==PIN)?-1:res[i]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
CF576E Painting Edges
简要题意
给定一张 \(n\) 个点、\(m\) 条边的无向图。一共有 \(p\) 种颜色,一开始每条边都没有颜色。
定义一种状态是合法的当且仅当仅保留染成 \(p\) 种颜色中的任何一种颜色的边,图都是一张二分图。
\(q\) 次操作,每次给定 \(e, c\),表示将第 \(e\) 条边染成颜色 \(c\)。但并不是每次操作都会被执行,只有当执行后仍然合法,才会执行本次操作。你需要判断每次操作是否会执行。
\(1 \leq n \leq 5 \times 10^5\),\(1 \leq m, q \leq 5 \times 10^5\),\(1 \leq p \leq 50\)。
其实只要想明白了上一道题,这一道题还是比较简单的。
首先,判断一次操作能否执行的方法是:对每种颜色,我们先维护出删去这条边的并查集,然后再将这条边加入颜色为 \(c\),判断是否使原来的二分图变成非二分图。也就是说我们要维护出删去这条边后的并查集,因此考虑线段树分治,将删除变成撤销。
考虑一次操作等价于将第 \(e\) 条边删去,并加入一条颜色为 \(c\) 的边。如果这次操作没有执行,为了与之前的操作向照应,我们也将这条边删去,并加入一条颜色相同的边。这样每条边的颜色就是固定不变的,我们将操作离线下来,然后将每条边按其出现的时间插入线段树。于是这样我们就可以在每个叶结点判断对应的操作是否能被执行。
但这样做有个问题,就是我们事先不知道每条边的颜色究竟会不会改变。处理方法与上一题类似,我们先不管每条边的颜色,把所有边都插进线段树,然后在判断操作是否合法的时候动态维护出下一条边的颜色即可。时间复杂度为 \(\Theta(np + q \log q \log n)\)。
代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
const int N=500003,M=53,log_N=20;
struct Edge{
int u,v;
}edge[N];
struct Operation{
int k,x;
}op[N];
int n,m,p,q,col[N],tim[N]; bool res[N];
struct DSU{
int fa[N],rnk[N],len,his[N]; bool dis[N];
DSU(){ len=0; }
void init_DSU(int n){
for(int i=1;i<=n;i++)
fa[i]=i,rnk[i]=1,dis[i]=0;
}
int find(int u){
while(fa[u]!=u) u=fa[u];
return u;
}
bool query(int u){
bool ans=0;
while(fa[u]!=u) ans^=dis[u],u=fa[u];
return ans;
}
bool merge(int u,int v,bool w){
int su=find(u),sv=find(v); bool ans=1;
if(rnk[su]<rnk[sv]) swap(su,sv);
if(su==sv) ans=((query(u)^query(v))==w),his[++len]=-1;
else{
dis[sv]=query(u)^query(v)^w;
rnk[su]+=rnk[sv],fa[sv]=su,his[++len]=sv;
}
return ans;
}
void rollback(){
int u=his[len--]; if(u==-1) return ;
rnk[fa[u]]-=rnk[u],dis[u]=0,fa[u]=u;
}
}dsu[M];
namespace SGT{
struct Node{
int l,r;
}node[(1<<log_N)+3];
vector<int> arr[(1<<log_N)+3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,int x){
if(l>r) return ;
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,bool res[N]){
for(int i=0;i<arr[u].size();i++){
if(!col[arr[u][i]]) continue;
dsu[col[arr[u][i]]].merge(edge[arr[u][i]].u,edge[arr[u][i]].v,1);
}
if(node[u].l==node[u].r){
int s1=node[u].l,s2=op[s1].k;
res[s1]=dsu[op[s1].x].merge(edge[s2].u,edge[s2].v,1),dsu[op[s1].x].rollback();
if(res[s1]) col[s2]=op[s1].x;
}else solve(u*2,res),solve(u*2+1,res);
for(int i=arr[u].size()-1;i>=0;i--)
if(col[arr[u][i]]) dsu[col[arr[u][i]]].rollback();
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
int main(){
// freopen("edge.in","r",stdin);
// freopen("edge.out","w",stdout);
n=read(),m=read(),p=read(),q=read();
for(int i=1;i<=m;i++)
edge[i].u=read(),edge[i].v=read();
SGT::build(1,1,q);
for(int i=1;i<=p;i++) dsu[i].init_DSU(n);
for(int i=1;i<=q;i++){
op[i].k=read(),op[i].x=read();
SGT::insert(1,tim[op[i].k]+1,i-1,op[i].k);
tim[op[i].k]=i;
}
for(int i=1;i<=m;i++)
SGT::insert(1,tim[i]+1,q,i);
SGT::solve(1,res);
for(int i=1;i<=q;i++)
puts(res[i]?"YES":"NO");
// fclose(stdin);
// fclose(stdout);
return 0;
}
洛谷 P9168 [省选联考 2023] 人员调度
简要题意
公司的 \(n\) 个部门构成一个以 \(1\) 号部门为根的有根树。初始时公司内有 \(k\) 名员工,第 \(i\) 名员工用一个二元组 \((x_i, v_i)\) 来表示,表示他初始时在第 \(x_i\) 个部门工作,且有一个能力值 \(v_i > 0\)。
为了最大化公司的业绩,公司老板决定进行一些人员调度。具体来说,可以将 \(i\) 号员工调动到 \(x_i\) 子树内的某个部门,或者不调度。随后,定义第 \(i\) 个部门的贡献值为其现有员工的最大能力值,如果该部门目前没有员工则其贡献值为 \(0\)。定义公司的业绩为各部门的贡献值之和。
\(m\) 次事件,每次事件为以下两种类型之一:
- \(1~x~v\):先令 \(k \leftarrow k + 1\),然后新增一位编号为 \(k\)、初始部门为 \(x\)、能力值为 \(v\) 的员工。
- \(2~\mathrm{id}\):编号为 \(\mathrm{id}\) 的员工被辞退。
公司老板希望你能在最开始和每个事件发生后,告诉他公司业绩的最大可能值。注意每次人员调动都是独立的,也就是每次计算公司的最大可能业绩时,每个员工都会回到其所在的初始部门。
\(1 \leq n, k \leq 10^5\),\(0 \leq m \leq 10^5\),\(1 \leq v_i, v \leq 10^5\)。
首先考虑没有修改怎么做,假设我们现在要求出 \(u\) 的子树的答案。考虑 \(u\) 的每个儿子 \(v\),显然 \(v\) 的子树中只能接受从 \(u\) 调来的人员,并且 \(u\) 的人员可以调往 \(v\) 子树中的所有结点。因此我们可以先贪心地求出每个 \(v\) 子树中的答案,再决策将 \(u\) 中的人员调往哪些结点。
于是这个过程就相当于自底向上的贪心,对每个点 \(u\) 我们需要决策将 \(u\) 中的人员调往哪些结点。事实上由于贪心是自底向上进行的,对于 \(u\) 以及在它后面决策的点来说,\(u\) 中所有结点其实是本质相同的,因为这个点要么与 \(u\) 无关,要么可以将人员调往 \(u\) 子树中的任意一个点。因此我们不妨在贪心的过程中维护一个集合表示它子树中每个结点的贡献值,那么当贪心到结点 \(u\) 时,我们需要先合并它所有儿子的集合,然后依次用结点 \(u\) 上的人员替换集合中的最小值(即先将这些人员插入集合,再不断弹出集合中的最小值,直到集合大小与子树大小相等)。这个集合可以用堆维护,合并的时候使用启发式合并,如果默认 \(n, m, q\) 同阶,时间复杂度为 \(\Theta(n \log^2 n)\)。
然后考虑有加人操作该怎么办,一个朴素的思路是我们动态维护每个结点上集合的变化。首先考虑这个员工 \((k, x, v)\) 需要加入哪些集合,那么我们从结点 \(x\) 往上找到第一个点 \(u\),满足 \(u\) 的集合中最小值大于 \(v\),那么对于 \(x\) 到 \(u\) 的链上每个点(不包含 \(u\)),它的集合中都要加入 \(k\) 号员工。然后我们考虑这些集合中需要删除哪些元素,如果仔细想想元素间的大小关系、在哪个点被弹出,我们会发现这个过程非常复杂,难以用常规数据结构维护。
这里我们运用化简的思想,题目中只要求最终哪些员工会保留下来,而且由于现在只加人,已经被删掉的人在增加员工后也会被删掉。因此对于每个结点的集合,我们只需维护最终被保留下来的人员,这样人员间的关系就不会因为中途的删除操作变得错综复杂了。此时加人操作就容易维护了:如果 \(x\) 号点到根的路径上每个结点的集合都没有满(集合大小小于子树大小),那么直接将 \(k\) 号员工插入这些集合即可;否则我们从 \(x\) 向上找到第一个已满的集合,记该结点为 \(u\) 且其中的最小贡献值为 \(t\),那么我们肯定只会在 \(v\) 和 \(t\) 之间删除一个元素,而且无论删除哪个元素,路径上剩余集合的大小都不会发生变化。那么我们要么将 \(v\) 插入 \(x\) 以上的每个集合,并将 \(t\) 从 \(u\) 以上的每个集合中删去,要么仅将 \(v\) 插入 \(x, u\) 之间的集合。
因此我们需要做的是:维护每个结点的“子树大小 - 集合大小”的值,支持链加、链上查询最小值;对于每个部门,维护最终保留下来且初始部门为它的所有人员,支持子树查询最小值。第一个可以使用树链剖分 + 线段树维护,第二个可以对每个结点建一个堆维护所有人员,并用线段树维护每个结点堆中的最小值。时间复杂度为 \(\Theta(n \log^2 n)\)。
现在有删除操作,那么我们直接上线段树分治,把删除操作变成撤销操作即可。时间复杂度为 \(\Theta(n \log^3 n)\)。
代码
#include <cstdio>
#include <cstring>
#include <functional>
#include <iostream>
#include <set>
#include <vector>
using namespace std;
const int N=100003;
const int PIN=1061109567;
struct Staff{
int k,x;
}a[N*2];
int n,m,q,fa[N],arr[N],dfn[N],tim[N*2];
int dep[N],son[N],hson[N],hroot[N];
int len_list=0,e[N],ne[N],h[N];
int len=0,his[N*2],val[N*2];
long long sum=0,res[N];
multiset<int> hp[N];
void add_once(int a,int b){
e[len_list]=b;
ne[len_list]=h[a];
h[a]=len_list++;
}
void add_twice(int a,int b){
add_once(a,b);
add_once(b,a);
}
void dfs1(int u,int depth){
dep[u]=depth,son[u]=0,hson[u]=-1;
for(int i=h[u];i>=0;i=ne[i]){
dfs1(e[i],depth+1),son[u]+=son[e[i]]+1;
if(hson[u]<0||son[e[i]]>son[hson[u]]) hson[u]=e[i];
}
}
void dfs2(int u,int rfa,int& p){
hroot[u]=rfa,arr[++p]=u,dfn[u]=p;
if(son[u]) dfs2(hson[u],rfa,p);
for(int i=h[u];i>=0;i=ne[i])
if(e[i]!=hson[u]) dfs2(e[i],e[i],p);
}
namespace SGT2{
struct Node{
int l,r,minn,minu,add;
}node[N*3];
Node operator+(Node l,Node r){
Node ans={l.l,r.r,min(l.minn,r.minn),0,0};
ans.minu=(ans.minn==r.minn)?r.minu:l.minu;
return ans;
}
void update(int u,int x){
node[u].minn+=x,node[u].add+=x;
}
void push_up(int u){
node[u]=node[u*2]+node[u*2+1];
}
void push_down(int u){
if(node[u].add!=0){
update(u*2 ,node[u].add);
update(u*2+1,node[u].add);
node[u].add=0;
}
}
void build(int u,int l,int r){
if(l==r) node[u]={l,r,son[arr[l]]+1,arr[l],0};
else{
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
push_up(u);
}
}
void modify(int u,int l,int r,int x){
if(node[u].l>=l&&node[u].r<=r) update(u,x);
else{
push_down(u);
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) modify(u*2 ,l,r,x);
if(r> mid) modify(u*2+1,l,r,x);
push_up(u);
}
}
Node query(int u,int l,int r){
if(node[u].l>=l&&node[u].r<=r) return node[u];
int mid=(node[u].l+node[u].r)/2; push_down(u);
if(r<=mid) return query(u*2 ,l,r);
if(l> mid) return query(u*2+1,l,r);
return query(u*2,l,r)+query(u*2+1,l,r);
}
}
namespace SGT3{
struct Node{
int l,r,minn,minu;
}node[N*3];
Node operator+(Node l,Node r){
Node ans={l.l,r.r,min(l.minn,r.minn),0};
ans.minu=(ans.minn==r.minn)?r.minu:l.minu;
return ans;
}
void push_up(int u){
node[u]=node[u*2]+node[u*2+1];
}
void build(int u,int l,int r){
node[u]={l,r,PIN,arr[l]};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
push_up(u);
}
}
void modify(int u,int k,int x){
if(node[u].l==node[u].r) node[u].minn=x;
else{
int mid=(node[u].l+node[u].r)/2;
modify(u*2+(k>mid),k,x),push_up(u);
}
}
Node query(int u,int l,int r){
if(node[u].l>=l&&node[u].r<=r) return node[u];
int mid=(node[u].l+node[u].r)/2;
if(r<=mid) return query(u*2 ,l,r);
if(l> mid) return query(u*2+1,l,r);
return query(u*2,l,r)+query(u*2+1,l,r);
}
}
void modify_path(int u,int x){
while(u>0) SGT2::modify(1,dfn[hroot[u]],dfn[u],x),u=fa[hroot[u]];
}
int query_path(int u){
SGT2::Node s1;
while(u>0){
s1=SGT2::query(1,dfn[hroot[u]],dfn[u]);
if(s1.minn<0) return s1.minu; else u=fa[hroot[u]];
}
return -1;
}
namespace SGT1{
struct Node{
int l,r;
}node[N*3];
vector<Staff> arr[N*3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,Staff x){
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve(int u,long long res[N]){
int i,s1,s2,s3,s4;
for(i=0;i<arr[u].size();i++){
s1=arr[u][i].k,s2=arr[u][i].x,sum+=s2;
if(s2<*hp[s1].begin()) SGT3::modify(1,dfn[s1],s2);
hp[s1].insert(s2),modify_path(s1,-1),his[++len]=-1;
if((s4=query_path(s1))==-1) continue;
his[len]=s3=SGT3::query(1,dfn[s4],dfn[s4]+son[s4]).minu;
val[len]=*hp[s3].begin(),sum-=*hp[s3].begin();
hp[s3].erase(hp[s3].begin()),modify_path(s3,1);
SGT3::modify(1,dfn[s3],*hp[s3].begin());
}
if(node[u].l==node[u].r) res[node[u].l]=sum;
else solve(u*2,res),solve(u*2+1,res);
for(i=arr[u].size()-1;i>=0;i--){
s1=arr[u][i].k,s2=arr[u][i].x;
s3=his[len],s4=val[len],len--;
if(s3>0){
sum+=s4,hp[s3].insert(s4);
SGT3::modify(1,dfn[s3],s4),modify_path(s3,-1);
}
sum-=s2,hp[s1].erase(hp[s1].find(s2)),modify_path(s1,1);
SGT3::modify(1,dfn[s1],*hp[s1].begin());
}
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
void write(long long x){
char a[12]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar(' ');
}
int main(){
// freopen("transfer.in","r",stdin);
// freopen("transfer.out","w",stdout);
int i,s1; read();
n=read(),m=read(),q=read();
memset(h,-1,sizeof h);
for(i=2;i<=n;i++) fa[i]=read();
for(i=2;i<=n;i++) add_once(fa[i],i);
dfs1(1,1),dfs2(1,1,s1=0);
SGT1::build(1,0,q);
for(i=1;i<=m;i++)
a[i].k=read(),a[i].x=read(),tim[i]=0;
for(i=1;i<=q;i++)
switch(read()){
case 1: m++,a[m].k=read(),a[m].x=read(),tim[m]=i; break;
case 2: s1=read(),SGT1::insert(1,tim[s1],i-1,a[s1]),tim[s1]=-1;
}
for(i=1;i<=m;i++)
if(tim[i]>=0) SGT1::insert(1,tim[i],q,a[i]);
for(i=1;i<=n;i++) hp[i].insert(PIN);
SGT2::build(1,1,n),SGT3::build(1,1,n),SGT1::solve(1,res);
for(i=0;i<=q;i++) write(res[i]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
洛谷 P5244 [USACO19FEB] Mowing Mischief P
简要题意
记矩形 \((p_1, q_1) - (p_2, q_2)\) 表示以 \((p_1, q_1)\) 为左下角、\((p_2, q_2)\) 为右上角的矩形区域。
现有矩形区域 \((0, 0) - (T, T)\),在这个区域中给定 \(n\) 个点,第 \(i\) 个点的坐标为 \((x_i, y_i)\)。保证 \(1 \leq x_i, y_i < T\) 且每个点的横坐标两两不同,纵坐标两两不同。
现我们需要在这 \(n\) 个点中选出若干个点构成点集 \(S\)。称点集 \(S\) 是合法的当且仅当将点集中的所有点按横坐标升序排序后,纵坐标也单调递增。对于合法点集 \(S\),我们按如下方式定义它的权值:
- 先将所有点按横坐标升序排序,记 \(S\) 中的点依次为 \((a_1, b_1), (a_2, b_2), \cdots, (a_k, b_k)\)。
- 分别计算矩形 \((0, 0) - (a_1, b_1), (a_1, b_1) - (a_2, b_2), (a_2, b_2) - (a_3, b_3), \cdots, (a_{k - 1}, b_{k - 1}) - (a_k, b_k), (a_k, b_k) - (T, T)\) 的面积。
- 合法点集 \(S\) 的权值即为上述 \(k + 1\) 个矩形的面积之和。
你的任务是找出一个合法点集 \(S\),在最大化 \(S\) 中点的数量的基础上,最小化 \(S\) 的权值。你只需要输出这个权值即可。
\(1 \leq n \leq 2 \times 10^5\),\(1 \leq T \leq 10^6\)。
显然一个合法的点集对应一条从 \((0, 0)\) 跳到 \((T, T)\) 的路径,总共跳了 \(k + 1\) 步,途中依次经过点 \((a_1, b_1), (a_2, b_2), \cdots, (a_k, b_k)\)。定义从 \((p_1, q_1)\) 跳一步到 \((p_2, q_2)\) 的代价为矩形 \((p_1, q_1) - (p_2, q_2)\) 的面积,则合法点集的权值即为从 \((0, 0)\) 跳到 \((T, T)\) 的代价之和。
考虑 DP,设 \(g(i)\) 表示从 \((0, 0)\) 跳到 \((x_i, y_i)\) 的过程中最多经过多少个点,\(f(i)\) 表示从 \((0, 0)\) 跳到 \((x_i, y_i)\) 的过程在中间点数取到最大值时,其代价和的最小值。转移时枚举上一个点跳到哪里,即对于满足 \(x_j < x_i, y_j < y_i\) 的点 \(j\),可以从点 \(j\) 转移到点 \(i\),且新增 \(1\) 个点、\((x_i - x_j)(y_i - y_j)\) 的代价,以 \(g\) 为第一关键字、\(f\) 为第二关键字取最小值即可。时间复杂度为 \(\Theta(n^2)\),无法接受。
考虑优化,由于 \(g\) 实质上求的是将所有点按横坐标排序后,纵坐标的最长上升子序列,因此我们不妨先求出 \(g\),再在 \(g\) 的基础上转移 \(f\)。\(g\) 的转移可以用树状数组优化至 \(\Theta(n \log T)\)。对于 \(f\),上述操作看似只是剥离了 \(f, g\) 的转移过程,实际上暗中钦定了 \(f\) 的转移顺序。我们需要根据 \(g(i)\) 逐层转移 \(f(i)\),即先将 \(g(i) = 0\) 的状态转移到 \(g(i) = 1\) 的状态,再将 \(g(i) = 1\) 的状态转移到 \(g(i) = 2\) 的状态,以此类推。
接下来我们只需考虑每一层的转移。上述操作的好处是每一层的点是有位置关系的,如果将它们按横坐标升序排序,那么纵坐标单调递减。也就是说,如果定义 \(w(j, i)\) 表示从点 \(j\) 转移到点 \(i\) 的代价 \((x_i - x_j)(y_i - y_j)\),那么 \(w(j, i)\) 满足反向的四边形不等式。这意味着如果点 \(j, k(x_j < x_k, y_j > y_k)\) 都能转移到点 \(i\) 且 \(f(j) + w(j, i) \geq f(k) + w(k, i)\),那么对于横坐标比 \(i\) 更大、纵坐标比 \(i\) 更小的点 \(i'\) 必有 \(f(j) + w(j, i') \geq f(k) + w(k, i')\)。但由于有转移范围的限制,\(f\) 的转移并不满足递减的决策单调性,因此我们无法直接使用分治或二分栈来优化。
因此我们考虑使用线段树分治去掉区间的限制,假设前一层的点 \(j\) 能够转移到后一层的第 \(l\) 到 \(r\) 个点,那么我们将 \([l, r]\) 拆分成线段树上的 \(\Theta(\log n)\) 个区间,然后将点 \(j\) 挂到这 \(\Theta(\log n)\) 个区间结点上。那么对于线段树上表示区间 \([l, r]\) 的结点,它里面挂着的结点一定能转移到后一层的第 \(l\) 到 \(r\) 个点,这样我们就去掉了区间的限制。因此我们对线段树上的每一个结点,都跑一遍决策单调性分治即可。
时间复杂度为 \(\Theta(n \log T + n \log^2 n)\)。
代码
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int N=200003,M=(int)1e6+3;
const long long PIN=1e18;
struct Point{
int x,y;
Point(){ x=y=0; }
Point(int _x,int _y){
x=_x,y=_y;
}
bool operator<(const Point u)const{
return x<u.x;
}
}a[N];
int n,m,g[N],ne[N],h[M];
vector<int> p1,p2; long long f[N];
inline int lowbit(int x){
return x&-x;
}
namespace BIT{
int n,node[M];
void init(int len){ n=len; }
void modify(int u,int x){
for(int i=u;i<=n;i+=lowbit(i))
node[i]=max(node[i],x);
}
int query(int u){
int ans=0;
for(int i=u;i>0;i-=lowbit(i))
ans=max(ans,node[i]);
return ans;
}
}
namespace SGT{
struct Node{
int l,r;
}node[N*3];
vector<int> arr[N*3];
void build(int u,int l,int r){
node[u]={l,r},arr[u].clear();
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert(int u,int l,int r,int x){
if(l>r) return ;
if(node[u].l>=l&&node[u].r<=r)
arr[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert(u*2 ,l,r,x);
if(r> mid) insert(u*2+1,l,r,x);
}
}
void solve2(int l,int r,int pl,int pr,const vector<int>& p1){
if(l>r||pl>pr) return ;
int mid=(l+r)/2,i,minu=-1;
long long minn=PIN,s1;
for(i=pl;i<=pr;i++){
s1=1ll*(a[p2[mid]].x-a[p1[i]].x)*(a[p2[mid]].y-a[p1[i]].y);
if(f[p1[i]]+s1<minn) minn=f[p1[i]]+s1,minu=i;
}
f[p2[mid]]=min(f[p2[mid]],minn);
solve2(l,mid-1,minu,pr,p1),solve2(mid+1,r,pl,minu,p1);
}
void solve1(int u){
solve2(node[u].l,node[u].r,0,arr[u].size()-1,arr[u]);
if(node[u].l<node[u].r) solve1(u*2),solve1(u*2+1);
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
int main(){
// freopen("mischief.in","r",stdin);
// freopen("mischief.out","w",stdout);
int i,j,k,l; n=read(),m=read();
for(i=1;i<=n;i++)
a[i].x=read(),a[i].y=read();
a[++n]=Point(m,m),sort(a+1,a+n+1);
BIT::init(m+1),BIT::modify(1,0);
for(i=1;i<=n;i++){
g[i]=BIT::query(a[i].y)+1;
BIT::modify(a[i].y+1,g[i]);
}
memset(h,-1,sizeof h);
for(i=0;i<=n;i++) ne[i]=h[g[i]],h[g[i]]=i;
for(i=1;i<=g[n];i++){
p1.clear(),p2.clear();
for(j=h[i-1];j>=0;j=ne[j]) p1.push_back(j);
for(j=h[i ];j>=0;j=ne[j]) p2.push_back(j);
reverse(p1.begin(),p1.end());
reverse(p2.begin(),p2.end());
SGT::build(1,0,p2.size()-1);
for(j=0,k=l=0;j<p1.size();j++){
while(k<p2.size()&&a[p2[k]].x<a[p1[j]].x) k++;
while(l<p2.size()&&a[p2[l]].y>a[p1[j]].y) l++;
SGT::insert(1,k,l-1,p1[j]);
}
for(j=0;j<p2.size();j++) f[p2[j]]=PIN;
SGT::solve1(1);
}
printf("%lld",f[n]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
洛谷 P7457 [CERC2018] The Bridge on the River Kawaii
简要题意
有一张 \(n\) 个点的带边权无向图,点从 \(0\) 开始编号。初始时图中没有边。\(q\) 次操作,每次操作为以下两种类型之一:
- \(0~x~y~v\):新增一条连接点 \(x, y\) 且边权为 \(v\) 的无向边;
- \(1~x~y\):删除图中连接点 \(x, y\) 的边;
- \(2~x~y\):询问从 \(x\) 到 \(y\) 的路径上最大边权的最小值。如果 \(x, y\) 不连通,输出 \(-1\)。
\(2 \leq n \leq 10^5\),\(1 \leq q \leq 10^5\),\(0 \leq v \leq 10\)。
注意到边权在 \([0, 10]\) 之间,因此考虑借鉴 ABC355F MST Query 的思路,对每种边权建一个并查集,在并查集 \(i(0 \leq i \leq 10)\) 存储边权不超过 \(i\) 的所有边。那么对于一次询问,我们只需要找到最小的 \(i\) 满足 \(x, y\) 在并查集 \(i\) 中联通即可。
问题转化为动态维护图的连通性,支持动态加边、删边。那么直接上线段树分治,把删边变成撤销即可。时间复杂度为 \(\Theta(q \log q \log n)\)。
代码
#include <cstdio>
#include <iostream>
#include <map>
#include <vector>
using namespace std;
const int N=100003,M=10;
struct Edge{
int u,v,w;
Edge(int _u,int _v,int _w=0){
u=min(_u,_v),v=max(_u,_v),w=_w;
}
bool operator<(const Edge x)const{
return (u!=x.u)?(u<x.u):(v<x.v);
}
};
int n,m,q=0,res[N]; map<Edge,int> mp;
struct DSU{
int fa[N],rnk[N],len,his[N];
DSU(){ len=0; }
void init(int n){
for(int i=1;i<=n;i++)
fa[i]=i,rnk[i]=1;
}
int find(int u){
while(fa[u]!=u) u=fa[u];
return u;
}
void merge(int u,int v){
u=find(u),v=find(v);
if(rnk[u]<rnk[v]) swap(u,v);
if(u==v) his[++len]=-1;
else rnk[u]+=rnk[v],his[++len]=v,fa[v]=u;
}
void rollback(){
int u=his[len--]; if(u<0) return ;
rnk[fa[u]]-=rnk[u],fa[u]=u;
}
bool query(int u,int v){
return find(u)==find(v);
}
}dsu[M+1];
namespace SGT{
struct Node{
int l,r;
}node[N*3];
vector<Edge> op[N*3],qu[N*3];
void build(int u,int l,int r){
node[u]={l,r};
if(l<r){
int mid=(l+r)/2;
build(u*2 ,l,mid );
build(u*2+1,mid+1,r);
}
}
void insert_op(int u,int l,int r,const Edge x){
if(node[u].l>=l&&node[u].r<=r)
op[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
if(l<=mid) insert_op(u*2 ,l,r,x);
if(r> mid) insert_op(u*2+1,l,r,x);
}
}
void insert_qu(int u,int k,const Edge x){
if(node[u].l==node[u].r) qu[u].push_back(x);
else{
int mid=(node[u].l+node[u].r)/2;
insert_qu(u*2+(k>mid),k,x);
}
}
void solve(int u,int res[N]){
for(int i=0;i<op[u].size();i++)
for(int j=op[u][i].w;j<=M;j++)
dsu[j].merge(op[u][i].u,op[u][i].v);
if(node[u].l==node[u].r)
for(int i=0,j;i<qu[u].size();i++){
for(j=0;j<=M;j++)
if(dsu[j].query(qu[u][i].u,qu[u][i].v)) break;
res[qu[u][i].w]=j;
}
else solve(u*2,res),solve(u*2+1,res);
for(int i=op[u].size()-1;i>=0;i--)
for(int j=op[u][i].w;j<=M;j++) dsu[j].rollback();
}
}
int read(){
char ch; int x=0;
do ch=getchar();
while(ch<'0'||ch>'9');
while(ch>='0'&&ch<='9')
x=x*10+(ch-'0'),ch=getchar();
return x;
}
void write(int x){
if(x<0) putchar('-'),x=-x;
char a[12]; int n=0,i;
do a[++n]=x%10+'0',x/=10; while(x>0);
for(i=n;i>0;i--) putchar(a[i]);
putchar('\n');
}
int main(){
// freopen("bridge.in","r",stdin);
// freopen("bridge.out","w",stdout);
int i,opt,x,y; map<Edge,int>::iterator it;
n=read(),m=read(),SGT::build(1,1,m);
for(i=1;i<=m;i++){
opt=read(),x=read()+1,y=read()+1;
switch(opt){
case 0: mp[Edge(x,y,read())]=i; break;
case 1:
it=mp.find(Edge(x,y));
SGT::insert_op(1,it->second,i-1,it->first);
mp.erase(it); break;
case 2: SGT::insert_qu(1,i,Edge(x,y,++q));
}
}
for(i=0;i<=M;i++) dsu[i].init(n);
for(it=mp.begin();it!=mp.end();++it)
SGT::insert_op(1,it->second,m,it->first);
for(SGT::solve(1,res),i=1;i<=q;i++)
write((res[i]>M)?-1:res[i]);
// fclose(stdin);
// fclose(stdout);
return 0;
}
浙公网安备 33010602011771号