lxl 数据结构(一)(3)

书接上回。Link

Day 5 可持久化数据结构 + 树上问题

20231206

还是讲了一些树套树问题和其余的树上问题。

可持久化线段树:P3919,P3834,P4592,P5795,P3567,P2839,LOJ6144,CF464E,CF543E,P3302,P7561

可持久化平衡树:P3835,P5055

DFS序列:CF383C,BZOJ3306,hdu5692,LOJ6276

树上差分:P1600,P2680,P4216

树链剖分:BZOJ3159,CF536E

树链剖分优化树上二分路径:两个题都没地方提交


Keynote

不要树链剖分学傻了!不是树上问题都是树链剖分。


对于树上的问题,我们首先考虑把它转化到序列上面怎么做,

这样把序列上面想好之后就可以轻易转到树上面去了。


对于一直往下走或者往上面走的问题,

我们按照套路可以进行树链剖分之后在重链上面二分,

于是问题就转化成 快速判断一条路径是否合法,这一般用线段树去维护。


对于主席树,并不是不可以进行区间修改,下面有一道例题会遇到,

这种情况我们直接把原来的节点赋值一遍再做操作即可。


对于中位数/平均数的问题,

我们一般都是用分数规划完成,即直接 二分


今天讲的东西有挺多套路的。(一时想不起来,后面补题的时候再补充吧)

Day 6 静态树分治

20231207


Keynote

\(k\) 小问题:

每次找路径上面最小的,直接删除即可。

(均摊复杂度)


点度数分治:当一个点的度数很大的时候我们考虑只维护它的轻儿子。

所有有关查儿子的信息都可以用

eg:P5314

树。

  1. 单点修改
  2. 查询一个点的所有儿子子树和的平方和

同样是我们只维护轻儿子的信息,

每一次修改就相当于链的修改,查询时统计一下重儿子的贡献即可。


讲边分治,点分治的时候太困了。。。

三度化优化相当于是把边分治和点分治结合起来。

树上启发式合并解决 子树 + lca 相关问题。


P4211 [LNOI2014] LCA

P4211 [LNOI2014] LCA

给出一个 \(n\) 个节点的有根树(编号为 \(0\)\(n-1\),根节点为 \(0\))。

一个点的深度定义为这个节点到根的距离 \(+1\)

\(dep[i]\) 表示点 \(i\) 的深度,\(\operatorname{LCA}(i, j)\) 表示 \(i\)\(j\) 的最近公共祖先。

\(m\) 次询问,每次询问给出 \(l, r, z\),求 \(\sum_{i=l}^r dep[\operatorname{LCA}(i,z)]\)

\(1 \le n,m \le 10^5\)

难得偶遇到一道做过的题。


用树剖和线段树维护,

我们考虑离线,将询问差分,

那么每一次扫到 \(i\) 节点就把 \(i\) 到根的路径上的全值都 \(+1\)


而我们查询的时候就是查询 \(z\) 到根上面的路径和即可。

可以画图理解一下。

Code
#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5,mod=201314;
int n,m,f[N],dep[N],siz[N],son[N],top[N],id[N],cnt=0,ans[N],head[N],tot=0;
struct edge{
  int v,nxt;
}e[N<<1];
struct node{
  int l,r,z;
}a[N];
vector<vector<int> > g(N);

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};
  head[u]=tot;
}

void dfs1(int u){
  dep[u]=dep[f[u]]+1;siz[u]=1;son[u]=-1;
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==f[u]) continue;
  	dfs1(v);
  	siz[u]+=siz[v];
  	if(son[u]==-1||siz[v]>siz[son[u]]) son[u]=v;
  }
}

void dfs2(int u,int pre){
  top[u]=pre;id[u]=++cnt;
  if(son[u]==-1) return ;
  dfs2(son[u],pre);
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==f[u]||v==son[u]) continue;
  	dfs2(v,v);
  }
}

struct seg{
  int val,tag;
}tr[N<<5];

#define mid ((l+r)>>1)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define lc rt<<1
#define rc rt<<1|1
void pushdown(int l,int r,int rt){
  if(tr[rt].tag){
  	tr[lc].tag=(tr[lc].tag+tr[rt].tag)%mod;
  	tr[rc].tag=(tr[rc].tag+tr[rt].tag)%mod;
  	tr[lc].val=(tr[lc].val+(mid-l+1)*tr[rt].tag%mod)%mod;
  	tr[rc].val=(tr[rc].val+(r-mid)*tr[rt].tag%mod)%mod;
  	tr[rt].tag=0;
  }
}

void pushup(int rt){tr[rt].val=(tr[lc].val+tr[rc].val)%mod;}

void update(int l,int r,int rt,int a,int b){
  if(a<=l&&b>=r){
  	tr[rt].tag=(tr[rt].tag+1)%mod;
  	tr[rt].val=(tr[rt].val+r-l+1)%mod;
  	return;
  }
  pushdown(l,r,rt);
  if(a<=mid) update(lson,a,b);
  if(b>mid) update(rson,a,b);
  pushup(rt);
}

int query(int l,int r,int rt,int a,int b){
  if(a<=l&&b>=r) return tr[rt].val;
  pushdown(l,r,rt);
  int res=0;
  if(a<=mid) res=(res+query(lson,a,b))%mod;
  if(b>mid) res=(res+query(rson,a,b))%mod;
  return res%mod;
}

void upd(int u,int v){
  while(top[u]!=top[v]){
  	if(dep[top[u]]<dep[top[v]]) swap(u,v);
  	update(1,n,1,id[top[u]],id[u]);
  	u=f[top[u]];
  }
  if(dep[u]>dep[v]) swap(u,v);
  update(1,n,1,id[u],id[v]);
}

int qry(int u,int v){
  int res=0;
  while(top[u]!=top[v]){
  	if(dep[top[u]]<dep[top[v]]) swap(u,v);
  	res=(res+query(1,n,1,id[top[u]],id[u]))%mod;
  	u=f[top[u]];
  }
  if(dep[u]>dep[v]) swap(u,v);
  res=(res+query(1,n,1,id[u],id[v]))%mod;
  return res;
}

int main(){
  /*2023.8.1 H_W_Y P4211 [LNOI2014] LCA 树链剖分+线段树*/ 
  n=read();m=read();
  for(int i=2;i<=n;i++){
  	f[i]=read();f[i]++;
  	add(f[i],i);
  }
  dfs1(1);dfs2(1,1);
  for(int i=1;i<=m;i++){
  	a[i].l=read();a[i].r=read();a[i].z=read();
  	a[i].l++;a[i].r++;a[i].z++;
  	g[a[i].l-1].push_back(i);
  	g[a[i].r].push_back(i);
  }
  for(int i=1;i<=n;i++){
  	upd(1,i);
  	for(int j=0;j<g[i].size();j++){
  	  if(a[g[i][j]].r==i) ans[g[i][j]]=(ans[g[i][j]]+qry(1,a[g[i][j]].z)+mod)%mod;
	  else ans[g[i][j]]-=qry(1,a[g[i][j]].z);	
    }
  }
  for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
  return 0;
}

CF757G

CF757G Can Bash Save the Day?

一棵 \(n\) 个点的树和一个排列 \(p_i\),边有边权,支持两种操作:

  • l r x,询问 \(\sum\limits _{i=l}^{r} dis(p_i,x)\)
  • x,交换 \(p_x,p_{x+1}\)

\(n,q\leq 2\times 10^5\),强制在线。

发现每次都只是交换相邻两个,于是直接暴力交换即可。


而对于查询,是类似与上一道题的,

但是强制在线,于是用套路的思路直接把扫描线变成主席树维护即可。

具体维护的还是和上一道题类似,因为相当于每次的 \(dis = dep[x]+dep[y]-2 \times dep[lca(x,y)]\)

所以我们需要算的还是 \(dep[lca(x,y)]\)


用什么树分治或者全局平衡二叉树等高级数据结构就可以做到单 \(\log\) 了。


CF696E

CF696E ...Wait for it...

给定一个 \(n\) 个节点的树,有 \(m\) 个物品,第 \(i\) 个在节点 \(c_i\),初始权值为 \(i\)。要求支持两种操作:

  • 对于树上的一条简单路径 \((u,v)\),删除权值前 \(k\) 小的物品并输出;
  • 对于一棵子树,将所有物品权值加 \(x\)

如果两个物品权值相同,编号更小的物品更小。

\(1 \le n,m,q \le 10^5\)

首先对于 \(k\) 小值,可以直接套用 Keynote 里面讲的。


于是这道题好像就做完了。

我们每一次输出最小的即可,用线段树维护一下。


P5314 [Ynoi2011]ODT - 点度数分治

P5314 [Ynoi2011]ODT

给一棵树,边权为 \(1\),支持:

  1. 把一条路径上所有点加上 \(k\)
  2. 查询距离一个点 \(\le 1\) 的所有点的点权 \(kth\)

\(1 \le n,m \le 2 \times 10^5,3s\)

首先感觉就可以用根号分治来做。(确实也可以)


考虑 polylog 做法。

每个点用一棵平衡树维护它的轻儿子的信息,

这样在每一次修改的时候之会对 \(\log\) 个节点的平衡树进行改变。

所以修改的查询复杂度是 \(\mathcal O(\log^2n)\) 的。


而在查询的时候我们发现有三个点是没有统计到的,

也就是它自己,父亲和重儿子。

我们直接在平衡树上面加上这三个点算出第 \(kth\) 大,再删除即可。


用了 gyy 的超级快读,平衡树删除两次都在 p 上面分裂浪费了一上午的时间。/kk

Code
#include <bits/stdc++.h>
using namespace std;
mt19937 rd(time(0));
#define pb push_back

namespace Fastio {
    #define USE_FASTIO 1
    #define IN_LEN 45000
    #define OUT_LEN 45000
    char ch, c; int len;
	short f, top, s;
    inline char Getchar() {
        static char buf[IN_LEN], *l = buf, *r = buf;
        if (l == r) r = (l = buf) + fread(buf, 1, IN_LEN, stdin);
        return (l == r) ? EOF : *l++;
    }
    char obuf[OUT_LEN], *ooh = obuf;
    inline void Putchar(char c) {
        if (ooh == obuf + OUT_LEN) fwrite(obuf, 1, OUT_LEN, stdout), ooh = obuf;
        *ooh++ = c;
    }
    inline void flush() { fwrite(obuf, 1, ooh - obuf, stdout); }

    #undef IN_LEN
    #undef OUT_LEN
    struct Reader {
        template <typename T> Reader& operator >> (T &x) {
            x = 0, f = 1, c = Getchar();
            while (!isdigit(c)) { if (c == '-') f *= -1; c = Getchar(); }
            while ( isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = Getchar();
            x *= f;
            return *this;
        }
        
        Reader() {}
    } cin;
    const char endl = '\n';
    struct Writer {
        typedef long long mxdouble;
        template <typename T> Writer& operator << (T x) {
            if (x == 0) { Putchar('0'); return *this; }
            if (x < 0) Putchar('-'), x = -x;
            static short sta[40];
            top = 0;
            while (x > 0) sta[++top] = x % 10, x /= 10;
            while (top > 0) Putchar(sta[top] + '0'), top--;
            return *this;
        }
        Writer& operator << (const char *str) {
            int cur = 0;
            while (str[cur]) Putchar(str[cur++]);
            return *this;
        }
        inline Writer& operator << (char c) {Putchar(c); return *this;}
        Writer() {}
        ~ Writer () {flush();}
    } cout;
	#define cin Fastio::cin
	#define cout Fastio::cout
	#define endl Fastio::endl
}


const int N=2e6+5;
int n,m,tot=0,rt[N],w[N];
vector<int> g[N];

namespace bt{
  int tr[N];
  int lowbit(int i){return i&(-i);}
  void upd(int x,int v){for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=v;}
  int qry(int x){int res=0;for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];return res;}
  void add(int x,int y,int v){upd(x,v);upd(y+1,-v);}
}

namespace fhq{
  struct node{int key,s[2],sz,v;}tr[N];
  int cnt=0;
  queue<int> q;
  
  #define lc(p) tr[p].s[0]
  #define rc(p) tr[p].s[1]
  
  int nwnode(int v){
  	int nw;
  	if(!q.empty()) nw=q.front(),q.pop();
  	else nw=++cnt;
  	tr[nw].key=rd();
  	tr[nw].sz=1;
  	tr[nw].s[0]=tr[nw].s[1]=0;
  	tr[nw].v=v;
  	return nw;
  }
  void pu(int p){tr[p].sz=tr[lc(p)].sz+tr[rc(p)].sz+1;}
  void split(int p,int k,int &x,int &y){
    if(!p){x=y=0;return;}
    if(tr[p].v<=k) x=p,split(rc(p),k,rc(p),y);
    else y=p,split(lc(p),k,x,lc(p));
    pu(p); 
  }

  int merge(int x,int y){
    if(!x||!y) return (x|y);
    if(tr[x].key<tr[y].key){
  	  tr[x].s[1]=merge(tr[x].s[1],y);
  	  pu(x);return x;
    }else{
  	  tr[y].s[0]=merge(x,tr[y].s[0]);
  	  pu(y);return y;
    }
  }
  void ins(int &p,int v){
  	int x=0,y=0;split(p,v,x,y);
  	p=merge(x,merge(nwnode(v),y));
  }
  void del(int &p,int v){
  	int x=0,y=0,z=0;
  	split(p,v-1,x,y);split(y,v,y,z);
  	q.push(y);y=merge(lc(y),rc(y));
    p=merge(x,merge(y,z));
  }
  void change(int &p,int v,int add){del(p,v);ins(p,v+add);}
  int find(int &p,int k){
  	int nw=p;
  	while(1){
  	  if(!nw) break;
  	  if(tr[lc(nw)].sz>=k) nw=lc(nw);
  	  else if(k==tr[lc(nw)].sz+1) break;
  	  else k-=tr[lc(nw)].sz+1,nw=rc(nw);
  	}
  	return tr[nw].v;
  }
}
using namespace fhq;

namespace sol{
  int dep[N],son[N],fa[N],sz[N],dfn[N],top[N],idx=0;
  void dfs1(int u,int pre){
  	dep[u]=dep[pre]+1,fa[u]=pre,sz[u]=1,son[u]=-1;
  	for(auto v:g[u]){
  	  if(v==pre) continue;
  	  dfs1(v,u);
  	  sz[u]+=sz[v];
  	  if(son[u]==-1||sz[v]>sz[son[u]]) son[u]=v;
  	}
  }
  void dfs2(int u,int pre){
    top[u]=pre;dfn[u]=++idx;
    if(son[u]==-1) return ;
    dfs2(son[u],pre);
    for(auto v:g[u]){
      if(v==fa[u]||v==son[u]) continue;
      dfs2(v,v);ins(rt[u],w[v]);
    }
  }
  int val(int x){return bt::qry(dfn[x])+w[x];}
  void upd(int x,int y,int v){
  	if(v==0) return;
  	while(top[x]!=top[y]){
  	  if(dep[top[x]]<dep[top[y]]) swap(x,y);
  	  change(rt[fa[top[x]]],val(top[x]),v);
  	  bt::add(dfn[top[x]],dfn[x],v);
  	  x=fa[top[x]];
  	}
  	if(dep[x]>dep[y]) swap(x,y);
  	if(top[x]==x&&fa[x]) change(rt[fa[x]],val(x),v); 
  	bt::add(dfn[x],dfn[y],v);
  }
  void init(int x,int op){
  	(op==0)?ins(rt[x],val(x)):del(rt[x],val(x));
  	if(fa[x]) (op==0)?ins(rt[x],val(fa[x])):del(rt[x],val(fa[x]));
  	if(son[x]!=-1) (op==0)?ins(rt[x],val(son[x])):del(rt[x],val(son[x]));
  }
  int qry(int x,int k){
    init(x,0);
	int res=find(rt[x],k);
	init(x,1);
    return res;
  }
}
using namespace sol;

int main(){
  /*2023.12.9 H_W_Y P5314 [Ynoi2011] ODT fhq+sp*/
  cin>>n>>m;memset(rt,0,sizeof(rt));
  for(int i=1;i<=n;i++) cin>>w[i];
  for(int i=1,u,v;i<n;i++) cin>>u>>v,g[u].pb(v),g[v].pb(u);
  dfs1(1,0);dfs2(1,1);
  for(int i=1,op,x,y,v;i<=m;i++){
  	cin>>op>>x>>y;
  	if(op==1) cin>>v,upd(x,y,v);
  	else cout<<qry(x,y)<<'\n';
  }
  return 0;
}

CF1017G The Tree

CF1017G The Tree

给定一棵树,维护以下3个操作:

  1. 1 x 表示如果节点 \(x\) 为白色,则将其染黑。否则对这个节点的所有儿子递归进行相同操作

  2. 2 x 表示将以节点 \(x\)\(root\) 的子树染白。

  3. 3 x 表示查询节点 \(x\) 的颜色

\(1 \le n,m \le 10^5\)

首先发现原来每个点都是白色的,

那么一个点 \(x\) 是黑色当且仅当 \(x\) 到根的路径存在一个点 \(y\)

使得 \(x \sim y\) 路径上的 \(1\) 操作个数 \(\ge dep[x]-dep[y]+1\)

也就说说把这条路上面的所有点都染黑了,而这是好维护的,

直接用树剖 + 线段树维护后缀最大值即可。


再来考虑有了操作二,

首先把子树中的操作清空是没有问题的。

但是有可能上面的点还会影响到它。


如果直接清空是对这个点的兄弟不满足条件,所以我们考虑找到后缀最大值 \(t\)

直接在 \(x\) 点上面 \(-t\) 即可。

于是这道题就做完了,实现用树剖 + 线段树即可。


P7880 Ynoi2006

P7880 [Ynoi2006] rldcot

给定一棵 \(n\) 个节点的树,树根为 \(1\),每个点有一个编号,每条边有一个边权。

定义 \(dep(x)\) 表示一个点到根简单路径上边权的和,\(lca(x,y)\) 表示 \(x,y\) 节点在树上的最近公共祖先。

\(m\) 组询问,每次询问给出 \(l,r\),求对于所有点编号的二元组 \((i,j)\) 满足 \(l \le i,j \le r\) ,有多少种不同的 \(dep( lca(i,j))\)

\(1 \le n \le 10^5,1 \le m \le 5 \times 10^5\)

首先可以考虑用莫队之类的乱搞,

但是这样过不了(会被 lxl 卡)。


我们考虑每一个点对答案的贡献,

不断的启发式合并,每一次对于一个子树中的数,我们用二分在前面的 set 中找到它的前驱和后继。

假设得到的区间是 \([a,b]\),那么只要 \(l \le a \&\&r \ge b\),这个点就有贡献。

而这个时候就很容易想到把它表示到二维平面上面了,这变成了一个 2-side 矩形。

我们对 \(dep\) 相同的去一个并即可,

一共有 \(n \log n\) 个矩形,因为这是启发式合并的次数。

于是总的时间复杂度就是 \(\mathcal O(n \log ^2n + m \log n)\)


Loj6145


Conclusion

  1. 平衡树一定不要写错了,注意分裂不要两次在同一个节点上面分裂。(P5314 [Ynoi2011]ODT)

Day 7 KDT + O(nlogn) 支配对

20231208

Keynote

树套树本质并不是树形结构,是一个 DAG,且不能维护复杂的标记,

也就是不能下传标记,只能标记永久化。


而 KDT 是一种维护矩形操作的数据结构,按照两维交替分治进行。

建树复杂度 \(\mathcal O(n \log n)\),查询复杂度 \(\mathcal O(\sqrt n)\)

不剪枝非常慢,常熟很大。


维护连续 \(1\) 段的个数 \(\to\) 有多少个 \(01/10 \to\) 有多少个二元组 \((i,i+1)\) 满足 \(b_i \oplus b_{i+1} =1\)

于是可以转化到一些奇妙的东西上面维护即可。


与 lca 相关的我们考虑树上启发式合并。


关于支配对,lxl 将他分成了两类:(直接贺

  • 第一类支配对:

    两两对象产生一个贡献,总共产生 \(O(n^2)\) 对贡献,但是这些贡献中 本质不同 的只有 \(O(n)\)

    常见的题型:树保留区间点

    一般这种问题在树上是常见的,有的树上问题要范围内的点两两点求出 LCA,这样是求范围内两两对象的贡献,但是 LCA 只有本质不同的 \(n\) 个点

    这种题的常见思路是做树上启发式合并,然后启发式合并时,加入点 \(x\) 时,只考虑编号为 \(x\) 的前驱后继的点和 \(x\) 产生二元组,这样会找出 \(O(n\log n)\) 个二元组,这些二元组可以支配原本 \(Θ(n^2)\) 对二元组的贡献

  • 第二类支配对:

    两两对象产生一个贡献,总共产生 \(O(n^2)\) 对贡献,但是这些贡献中本质不同的有 \(O(n^2)\)

    常见的题型:区间内两两算个东西然后求 \(\min\)\(\max\)

    有两种常见的支配对形式:

    第一种是对每个 \(i\),可以用数据结构高效找出 \(O(\log n)\)\(j\),这些 \((i,j)\) 的贡献支配了所有 \((i,1),(i,2),\dots (i,n)\) 的贡献

    第二种是对一维分治时,假设对大小 \(n\) 的问题进行分治,会产生 \(O(n)\) 对跨过分治中线的贡献,这些贡献支配了本来两边产生的 \(O(n^2)\) 对贡献,一般这种问题会对信息那一维分治,不对序列分治


P4475 巧克力王国 - KDT

P4475 巧克力王国

巧克力王国里的巧克力都是由牛奶和可可做成的。但是并不是每一块巧克力都受王国人民的欢迎,因为大家都不喜欢过于甜的巧克力。

对于每一块巧克力,我们设 \(x\)\(y\) 为其牛奶和可可的含量。由于每个人对于甜的程度都有自己的评判标准,所以每个人都有两个参数 \(a\)\(b\) ,分别为他自己为牛奶和可可定义的权重, 因此牛奶和可可含量分别为 \(x\)\(y\) 的巧克力对于他的甜味程度即为 \(ax+by\)。而每个人又有一个甜味限度 \(c\) ,所有甜味程度大于等于 \(c\) 的巧克力他都无法接受。每块巧克力都有一个美味值 \(h\)

现在我们想知道对于每个人,他所能接受的巧克力的美味值之和为多少。

对于100%的数据,\(1<=n,m<=50000\),\(-10^9<=a_i,b_i,x_i,y_i<=10^9\)

保证数据用某种方式随机生成。

只能说是 KDT 纯纯乱搞啊,想卡一下怎么都可以卡。


想不到吧,直接用 KDT 维护每一个点就可以了,

判断的时候就判一下一个矩形的端点是不是满足条件的,也就是判断是不是在直线的下面。

剪枝加上之后就挺快的。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long 

const int N=5e5+5;
int n,m,nw,rt;
ll a,b,c;
struct node{
  int d[2],mn[2],mx[2],v,lc,rc;
  ll s;
  friend bool operator <(node x,node y){return x.d[nw]<y.d[nw];}
}pt[N],tr[N];

void pu(int p){
  int lc=tr[p].lc,rc=tr[p].rc;
  for(int i=0;i<2;i++){
  	tr[p].mn[i]=tr[p].mx[i]=tr[p].d[i];
  	if(lc) tr[p].mn[i]=min(tr[p].mn[i],tr[lc].mn[i]),tr[p].mx[i]=max(tr[p].mx[i],tr[lc].mx[i]);
  	if(rc) tr[p].mn[i]=min(tr[p].mn[i],tr[rc].mn[i]),tr[p].mx[i]=max(tr[p].mx[i],tr[rc].mx[i]);
  }
  tr[p].s=tr[lc].s+tr[rc].s+tr[p].v;
}

int build(int l,int r,int fl){
  nw=fl;int mid=((l+r)>>1);
  nth_element(pt+l,pt+mid,pt+r+1);
  tr[mid]=pt[mid];
  if(l<mid) tr[mid].lc=build(l,mid-1,!fl);
  if(r>mid) tr[mid].rc=build(mid+1,r,!fl);
  pu(mid);return mid;
}

bool chk(ll x,ll y){return 1ll*x*a+1ll*y*b<c;}
ll qry(int p){
  int cnt=0;
  cnt+=chk(tr[p].mx[0],tr[p].mx[1]);
  cnt+=chk(tr[p].mx[0],tr[p].mn[1]);
  cnt+=chk(tr[p].mn[0],tr[p].mx[1]);
  cnt+=chk(tr[p].mn[0],tr[p].mn[1]);
  if(cnt==4) return tr[p].s;
  if(cnt==0) return 0;
  ll res=0;
  if(chk(tr[p].d[0],tr[p].d[1])) res+=tr[p].v;
  if(tr[p].lc) res+=qry(tr[p].lc);
  if(tr[p].rc) res+=qry(tr[p].rc);
  return res;
}

int main(){
  /*2023.12.19 H_W_Y P4475 巧克力王国 KDT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>pt[i].d[0]>>pt[i].d[1]>>pt[i].v;
  rt=build(1,n,0);
  for(int i=1;i<=m;i++){
  	cin>>a>>b>>c;
  	cout<<qry(rt)<<'\n';
  }
  return 0;
}

P3710 方方方的数据结构 - KDT

P3710 方方方的数据结构

在很久很久以前,有一个长度为 \(n\) 的数列,一开始数列全是 \(0\)

方方方觉得这个数列太单调了,打算对它进行 \(m\) 次操作,每次操作为区间加法或者区间乘法。

方方方进行一些操作之后,还可能会对某个数进行询问。

但是进行过一些操作之后,方方方可能会发现之前某次操作失误了,需要撤销这次操作,其它操作和其它操作的前后顺序保持不变。

\(1 \leq n,m \leq 150000\)\(1 \le l \le r \le n\)

感觉就不是特别好做。

首先考虑离线下来,那么我们维护每一个操作影响的时间区间,

于是就变成了一个二维的平面,一维是序列,一维是时间。


每一个询问操作就是一个点,

而每一次的修改就是一个矩形的修改。

于是我们对询问操作的那些点建出 KDT,于是直接维护矩形修改即可。


\(rc\) 写成 \(rt\) 已经是第二次了。/fn

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll mod=998244353;
const int N=2e5+5;
int n,m,nw,cnt=0,rt;
ll ans=0;
struct node{
  int op,l,r,pos;
  ll v;
}a[N];
struct KDT{
  int d[2],mn[2],mx[2],lc,rc;
  ll v,add,mul;
  friend bool operator <(KDT x,KDT y){return x.d[nw]<y.d[nw];}
}tr[N],dat[N];

void pu(int p){
  int lc=tr[p].lc,rc=tr[p].rc;
  for(int i=0;i<2;i++){
  	tr[p].mn[i]=tr[p].mx[i]=tr[p].d[i];
  	if(lc) tr[p].mn[i]=min(tr[p].mn[i],tr[lc].mn[i]),tr[p].mx[i]=max(tr[p].mx[i],tr[lc].mx[i]);
  	if(rc) tr[p].mn[i]=min(tr[p].mn[i],tr[rc].mn[i]),tr[p].mx[i]=max(tr[p].mx[i],tr[rc].mx[i]);
  }
  tr[p].add=0;tr[p].mul=1;
}

void padd(int p,ll v){(tr[p].v+=v)%=mod,(tr[p].add+=v)%=mod;}

void pmul(int p,ll v){(tr[p].v*=v)%=mod,(tr[p].add*=v)%=mod,(tr[p].mul*=v)%=mod;}

void pd(int p){
  int lc=tr[p].lc,rc=tr[p].rc;
  if(tr[p].mul>1) pmul(lc,tr[p].mul),pmul(rc,tr[p].mul),tr[p].mul=1ll;
  if(tr[p].add>0) padd(lc,tr[p].add),padd(rc,tr[p].add),tr[p].add=0;
}

int build(int l,int r,int fl){
  int mid=((l+r)>>1);nw=fl;
  nth_element(dat+l,dat+mid,dat+r+1);
  tr[mid]=dat[mid];
  if(l<mid) tr[mid].lc=build(l,mid-1,!fl);
  if(r>mid) tr[mid].rc=build(mid+1,r,!fl);
  pu(mid);
  return mid;
}

bool out(int p,int l1,int r1,int l2,int r2){
  return tr[p].mn[0]>r1||tr[p].mx[0]<l1||tr[p].mn[1]>r2||tr[p].mx[1]<l2;
}

bool in(int p,int l1,int r1,int l2,int r2){
  return tr[p].mn[0]>=l1&&tr[p].mx[0]<=r1&&tr[p].mn[1]>=l2&&tr[p].mx[1]<=r2;
}

bool chk(int p,int l1,int r1,int l2,int r2){
  return tr[p].d[0]>=l1&&tr[p].d[0]<=r1&&tr[p].d[1]>=l2&&tr[p].d[1]<=r2;
}

void mofadd(int p,int l1,int r1,int l2,int r2,ll v){
  if(out(p,l1,r1,l2,r2)) return;
  if(in(p,l1,r1,l2,r2)) return padd(p,v);
  if(chk(p,l1,r1,l2,r2)) (tr[p].v+=v)%=mod;
  pd(p);int lc=tr[p].lc,rc=tr[p].rc;
  if(lc) mofadd(lc,l1,r1,l2,r2,v);
  if(rc) mofadd(rc,l1,r1,l2,r2,v);
}

void mofmul(int p,int l1,int r1,int l2,int r2,ll v){
  if(out(p,l1,r1,l2,r2)) return;
  if(in(p,l1,r1,l2,r2)) return pmul(p,v);
  if(chk(p,l1,r1,l2,r2)) (tr[p].v*=v)%=mod;
  pd(p);int lc=tr[p].lc,rc=tr[p].rc;
  if(lc) mofmul(lc,l1,r1,l2,r2,v);
  if(rc) mofmul(rc,l1,r1,l2,r2,v);
}

void qry(int p,int x,int y){
  if(x<tr[p].mn[0]||x>tr[p].mx[0]||y<tr[p].mn[1]||y>tr[p].mx[1]) return;
  if(x==tr[p].d[0]&&y==tr[p].d[1]) return ans=tr[p].v,void();
  pd(p);int lc=tr[p].lc,rc=tr[p].rc;
  if(lc) qry(lc,x,y);
  if(rc) qry(rc,x,y); 
}

int main(){
  /*2023.12.19 H_W_Y P3710 方方方的数据结构 KDT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=m;i++){
  	cin>>a[i].op;
  	if(a[i].op<=2) cin>>a[i].l>>a[i].r>>a[i].v;
  	if(a[i].op==3) cin>>a[i].pos,dat[++cnt].d[0]=a[i].pos,dat[cnt].d[1]=i;
  	if(a[i].op==4) cin>>a[i].pos,a[a[i].pos].pos=i;
  }
  for(int i=1;i<=m;i++) if(a[i].op<=2&&!a[i].pos) a[i].pos=m;
  rt=build(1,cnt,0);
  for(int i=1;i<=m;i++){
  	if(a[i].op==1) mofadd(rt,a[i].l,a[i].r,i,a[i].pos,a[i].v);
    if(a[i].op==2) mofmul(rt,a[i].l,a[i].r,i,a[i].pos,a[i].v);
    if(a[i].op==3) qry(rt,a[i].pos,i),cout<<ans<<'\n'; 
  }
  return 0;
}

P8528 [Ynoi2003] 铃原露露 - 第一类支配对

P8528 [Ynoi2003] 铃原露露

给定一棵有根树,顶点编号为 \(1,2,\dots,n\),对 \(2\le i\le n\)\(f_{i}\)\(i\) 的父亲。\(a_1,\dots,a_n\)\(1,\dots,n\) 的排列。

\(m\) 次询问,每次询问给出 \(l,r\),询问有多少个二元组 \(L,R\),满足 \(l\le L\le R\le r\),且对任意 \(L\le a_x\le a_y\le R\),有 \(x,y\) 在树上的最近公共祖先 \(z\) 满足 \(L\le a_z\le R\)

以上所有数值为整数。

\(1 \le n,m \le 2 \times 10^5\)

好题啊。


一个套路的集合。

首先对于这种 LCA 问题,我们考虑树上启发式合并,

每次到一个点的时候直接在集合中 lower_bound 即可,这样是 \(\mathcal O(n \log ^2n)\) 的,

分析就是关于 \(z\)\(x,y\) 的大小关系即可。


这样我们得到了 \(\mathcal O(n \log^2n)\) 个矩形,

再跑一次扫描线,由于有值是不好处理的,

所以我们直接维护为 \(0\) 的个数即可。

又是维护历史值的和(感觉很烦)。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=2e5+5;
int n,m,a[N],rt[N],head[N],tot=0;
struct edge{int v,nxt;}e[N];
struct node{int l,r,v;};
vector<node> g[N],q[N];
set<int> s[N];
ll ans[N];

void add(int u,int v){e[++tot]=(edge){v,head[u]};head[u]=tot;}

int merge(int u,int v,int z){
  if(s[u].size()<s[v].size()) swap(u,v);
  int y;
  for(auto x:s[v]){
  	auto it=s[u].lower_bound(x);
  	if(it!=s[u].end()){
  	  y=*it;
  	  if(z<x) g[y].pb((node){z+1,x,1});
  	  if(z>y) g[y].pb((node){1,x,1}),g[z].pb((node){1,x,-1});
  	}
  	if(it==s[u].begin()) continue;
  	y=*(--it);
  	if(z<y) g[x].pb((node){z+1,y,1});
  	if(z>x) g[x].pb((node){1,y,1}),g[z].pb((node){1,y,-1});
  }
  for(auto nw:s[v]) s[u].insert(nw);
  return u;
}

void dfs(int u){
  rt[u]=u;s[u].insert(a[u]);
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	dfs(v);rt[u]=merge(rt[v],rt[u],a[u]);
  }
}

namespace SGT{
  struct sgt{
  	int tag,cnt,mn,add;
  	ll s;
  }tr[N<<2];

  #define mid ((l+r)>>1)
  #define lc p<<1
  #define rc p<<1|1
  #define lson l,mid,lc
  #define rson mid+1,r,rc
  
  void pu(int p){
  	tr[p].cnt=0;
  	tr[p].mn=min(tr[lc].mn,tr[rc].mn);
  	if(tr[lc].mn==tr[p].mn) tr[p].cnt+=tr[lc].cnt;
  	if(tr[rc].mn==tr[p].mn) tr[p].cnt+=tr[rc].cnt;
  	tr[p].s=tr[lc].s+tr[rc].s;
  }
  
  void padd(int p,int v){tr[p].mn+=v,tr[p].add+=v;}
  void ptag(int p,int v){tr[p].s+=1ll*v*tr[p].cnt,tr[p].tag+=v;}
  
  void pd(int p){
  	if(tr[p].add) padd(lc,tr[p].add),padd(rc,tr[p].add),tr[p].add=0;
  	if(tr[p].tag){
  	  if(tr[lc].mn==tr[p].mn) ptag(lc,tr[p].tag);
  	  if(tr[rc].mn==tr[p].mn) ptag(rc,tr[p].tag);
  	  tr[p].tag=0;
  	}
  }
  
  void build(int l=1,int r=n,int p=1){
    tr[p].cnt=r-l+1;
    if(l==r) return ;
    build(lson);build(rson);
  }
  
  void upd(int x,int y,int val,int l=1,int r=n,int p=1){
  	if(x<=l&&y>=r) return padd(p,val);pd(p);
  	if(x<=mid) upd(x,y,val,lson);
  	if(y>mid) upd(x,y,val,rson);pu(p);
  }
  
  void tag(int x,int y,int val,int l=1,int r=n,int p=1){
  	if(x<=l&&y>=r){
  	  if(!tr[p].mn) ptag(p,val);
  	  return;
  	}pd(p);
  	if(x<=mid) tag(x,y,val,lson);
  	if(y>mid) tag(x,y,val,rson);pu(p);
  }
  
  ll qry(int x,int y,int l=1,int r=n,int p=1){
  	if(x<=l&&y>=r) return tr[p].s;pd(p);
    if(y<=mid) return qry(x,y,lson);
    if(x>mid) return qry(x,y,rson);
    return qry(x,y,lson)+qry(x,y,rson);
  }
}
using namespace SGT;

int main(){
  /*2023.12.9 H_W_Y P8528 [Ynoi2003] 铃原露露 lca + sgt*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=2,x;i<=n;i++) cin>>x,add(x,i);
  dfs(1);build();
  for(int i=1,l,r;i<=m;i++) cin>>l>>r,q[r].pb((node){l,r,i});
  
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) upd(j.l,j.r,j.v);
  	tag(1,i,1);
  	for(auto j:q[i]) ans[j.v]=qry(j.l,j.r);
  }
  
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

CF765F Souvenirs - 第二类支配对

CF765F Souvenirs

给出 \(n\) 以及一个长为 \(n\) 的序列 \(a\)

给出 \(m\),接下来 \(m\) 组询问。

每组询问给出一个 \(l,r\),你需要求出,对于 \(i,j \in [l,r]\),且满足 \(i \neq j\)\(|a_i-a_j|\) 的最小值。

\(1 \leq n \leq 10^5\)\(1 \leq m \leq 3\times 10^5\)\(0 \leq a_i \leq 10^9\)

典。


首先对于每一个点,我们只考虑与它后面点的贡献,而只用考虑比它大的,

因为比它小的可以将值域翻转之后再做一次。

于是容易想到对于每一个 \(a_i\) 我们维护的其实是一个有关于 \(a_i\) 的递减序列,

也就是可能对答案有贡献的点就是图中红色的点(乍一想是这样的)。

![CF765F](E:\H_W_Y\0-C++#代码编程\cdqz\lxl 数据结构\pic\CF765F.png)

但是这样的点太多了,很明显无法完成。

所以我们考虑其实有一些点是不需要的,比如当前的 \(a_i\) 和第二个红点,

这种情况是可以被第一个红点和第二个红点给支配掉。

于是我们最开始得到的 \(\Delta\),会影响我们接下来的决策,也就是下一次一定是选 \(\Delta' \le \frac{\Delta}{2}\) 的点,

也就是直接选到了第三个红点。


利用这样的思路发现每一个点只会牵连到 \(\log n\) 个可以对答案产生贡献的点,

于是我们就得到了 \(n \log n\) 支配对,直接维护即可。

而找支配对可以直接用权值线段树维护最前面在一段值域中的数。

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<int,int> 
#define fi first
#define se second

const int N=5e5+5,inf=1e9;
int n,m,a[N],ans[N],rt;
vector<int> g[N];
vector<pii> q[N];

struct BIT{
  int tr[N];
  void init(){for(int i=1;i<=n;i++) tr[i]=inf;}
  int lowbit(int i){return i&(-i);}
  void upd(int x,int v){for(int i=x;i>=1;i-=lowbit(i)) tr[i]=min(tr[i],v);}
  int qry(int x){
  	int res=inf;
  	for(int i=x;i<=n;i+=lowbit(i)) res=min(res,tr[i]);
  	return res;
  }
}T;

struct SGT{
  int tr[N*50],lc[N*50],rc[N*50],idx=0;
  #define mid ((l+r)>>1)
  void init(){memset(tr,0x3f,sizeof(tr));idx=0;}
  void upd(int l,int r,int &p,int x,int v){
    if(!p) p=++idx,lc[p]=rc[p]=0;
    tr[p]=min(tr[p],v);
    if(l==r) return ;
    (x<=mid)?upd(l,mid,lc[p],x,v):upd(mid+1,r,rc[p],x,v);
  }
  int qry(int l,int r,int p,int x,int y){
    if(!p) return inf;
    if(x<=l&&y>=r) return tr[p];
    int res=inf;
    if(x<=mid) res=min(res,qry(l,mid,lc[p],x,y));
    if(y>mid) res=min(res,qry(mid+1,r,rc[p],x,y));
    return res;
  }
}sgt;

void pu(int u,int v){
  if(u>v) swap(u,v);
  g[v].pb(u);
}

void wrk(){
  sgt.init();rt=0;
  for(int i=n;i>=1;i--){
  	int pos=sgt.qry(0,inf,rt,a[i],inf);
  	while(pos<=n){
  	  pu(i,pos);
  	  int delta=abs(a[i]-a[pos]);
  	  if(delta==0) break;
  	  pos=sgt.qry(0,inf,rt,a[i],a[i]+(delta-1)/2);
  	}
  	sgt.upd(0,inf,rt,a[i],i);
  }
}

int main(){
  /*2023.12.19 H_W_Y CF765F Souvenirs 第二类支配对*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;T.init();
  for(int i=1;i<=n;i++) cin>>a[i];wrk();
  for(int i=1;i<=n;i++) a[i]=inf-a[i];wrk();
  cin>>m;
  for(int i=1,l,r;i<=m;i++){
  	cin>>l>>r;
  	q[r].pb({l,i});
  }
  for(int i=1;i<=n;i++){
  	sort(g[i].begin(),g[i].end());
  	g[i].resize(unique(g[i].begin(),g[i].end())-g[i].begin());
  	for(auto j:g[i]) T.upd(j,abs(a[i]-a[j]));
  	for(auto j:q[i]) ans[j.se]=T.qry(j.fi);
  }
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

P9058 [Ynoi2004] rpmtdq - 点分治 + 第二类支配对

P9058 [Ynoi2004] rpmtdq

给定一棵有边权的无根树,需要回答一些询问。

定义 \(\texttt{dist(i,j)}\) 代表树上点 \(i\) 和点 \(j\) 之间的距离。

对于每一组询问,会给出 \(l,r\),你需要输出 \(\min(\texttt{dist(i,j)})\) 其中 \(l\leq i < j \leq r\)

\(n\leq2\times 10^5\)\(q\leq 10^6\)\(1\le z\le 10^9\)

首先容易想到去找支配对,

这样就容易想到分治,我们使用点分治或者边分治都是可以的。


对于一个距离分治中心距离为 \(d\) 的点,我们希望找到的其实就是它的前驱和后继,

不然这一定会被支配掉。(不想分析不想分析不想分析/fn

感觉画一下图就知道了,于是只有 \(n \log n\) 个支配对,所以用扫描线 + 树状数组维护一下就可以了。

于是就做完了——直接上代码。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pli pair<ll,int>
#define pil pair<int,ll>
#define fi first
#define se second
#define pb push_back

const int N=2e5+5,M=1e6+5,INF=1e9;
const ll inf=1e18;
int n,m,head[N],tot=0,tp,tp2,sz[N];
ll ans[M];
bool vis[N];
pil st[N],st2[N];
struct edge{int v,nxt,w;}e[N<<1];
struct qry{int pos,id;};
vector<qry> q[N];
vector<int> g[N];

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],w};head[v]=tot;
}

void pu(int u,int v){if(u>v) swap(u,v);g[v].pb(u);}

struct DIS{
  int st[20][N],lg[N],dfn[N],idx=0;
  ll d[N];
  
  void dfs(int u,int fa){
    dfn[u]=++idx;
    st[0][idx]=fa;
    for(int i=head[u];i;i=e[i].nxt){
      int v=e[i].v,w=e[i].w;
      if(v==fa) continue;
      d[v]=d[u]+1ll*w;dfs(v,u);
    }
  }
  int Min(int u,int v){return dfn[u]<dfn[v]?u:v;}
  void init(){
    dfs(1,0);lg[1]=0;
    for(int i=2;i<=n;i++) lg[i]=lg[i/2]+1;
    for(int i=1;i<=lg[n]+1;i++)
      for(int j=1;j+(1<<i)-1<=n;j++) 
        st[i][j]=Min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
  }
  int lca(int u,int v){
    if(u==v) return u;
    u=dfn[u];v=dfn[v];
    if(u>v) swap(u,v);
    int s=lg[v-u];
    return Min(st[s][u+1],st[s][v-(1<<s)+1]);
  }
  ll dis(int u,int v){return d[u]+d[v]-2*d[lca(u,v)];}
}s;

struct BIT{
  ll tr[N];
  void init(){for(int i=0;i<=n;i++) tr[i]=inf;}
  int lowbit(int i){return i&(-i);}
  void upd(int x,ll val){for(int i=x;i>=1;i-=lowbit(i)) tr[i]=min(tr[i],val);}
  ll qry(int x){
  	ll res=inf;
  	for(int i=x;i<=n;i+=lowbit(i)) res=min(res,tr[i]);
  	return res;
  }
}T;

pii findrt(int u,int fa,int pre){
  sz[u]=1;
  pii res={INF,0};
  int mx=0;
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa||vis[v]) continue;
  	res=min(res,findrt(v,u,pre));
  	sz[u]+=sz[v];mx=max(mx,sz[v]);
  }
  res=min(res,{max(mx,pre-sz[u]),u});
  return res;
}

void dfs(int u,int fa,ll d){
  st[++tp]={u,d};
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;ll w=1ll*e[i].w;
  	if(v==fa||vis[v]) continue;
  	dfs(v,u,d+w);
  }
}

void sol(int u,int pre){
  int rt=findrt(u,0,pre).se,res=sz[rt];
  vis[rt]=true;tp=0;
  for(int i=head[rt];i;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;
  	if(vis[v]) continue;
  	dfs(v,rt,w);
  }
  st[++tp]={rt,0};
  sort(st+1,st+tp+1);
  tp2=0;
  for(int i=1;i<=tp;i++){
  	int id=st[i].fi;ll d=st[i].se;
  	while(tp2&&st2[tp2].se>d) --tp2;
  	pu(id,st2[tp2].fi);st2[++tp2]=st[i];
  }
  tp2=0;
  for(int i=tp;i>=1;i--){
  	int id=st[i].fi;ll d=st[i].se;
  	while(tp2&&st2[tp2].se>d) --tp2;
  	pu(id,st2[tp2].fi);st2[++tp2]=st[i];
  }
  for(int i=head[rt];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(vis[v]) continue;
  	sol(v,res);
  }
}

int main(){
  /*2023.12.19 H_W_Y P9058 [Ynoi2004] rpmtdq 点分治 + nlogn 支配对*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1,u,v,w;i<n;i++) cin>>u>>v>>w,add(u,v,w);
  
  s.init();T.init();sol(1,n);
  for(int i=1;i<=n;i++){
  	sort(g[i].begin(),g[i].end());
  	g[i].resize(unique(g[i].begin(),g[i].end())-g[i].begin());
  }
  
  cin>>m;
  for(int i=1,l,r;i<=m;i++){
  	cin>>l>>r;
  	if(l>=r) ans[i]=-1;
  	else q[r].pb((qry){l,i});
  }
  
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) T.upd(j,s.dis(i,j));
  	for(auto j:q[i]) ans[j.id]=T.qry(j.pos);
  }
  
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

CF407E k-d-sequence - 扫描线

CF407E k-d-sequence

找一个最长的子区间使得该子区间加入至多 \(k\) 个数以后,排序后是一个公差为 \(d\) 的等差数列。

多个解输出 \(l\) 最小的。

\(1 \le n \le 2 \times 10^5\)

典题,暑假的时候 TQX 似乎讲过,但很明显我们没听也不会。


首先需要特判掉 \(d=0\) 的情况,不然你会喜提 Runtime error on test 2。

然后我们再来考虑接下来的情况,

容易发现,一段区间是满足条件的,当且仅当以下三个条件都满足:

  1. 整个区间 \(\equiv x \pmod d\),也就是模 \(d\) 同余。
  2. \(\max - \min \le r-l+k\),其中 \(\max\)\(\min\) 都是除了 \(d\) 之后的值。
  3. 区间中没有相同的数。

于是第一步我们可以将整个序列按照 \(d\) 的余数分成很多段区间,于是就不需要考虑条件 \(1\) 了。

我们考虑用扫描线维护,扫过每一个右端点,维护每一个左端点,

查询的时候我们是查一个区间满足条件 \(2\) 的最小的 \(l\),而这个区间的端点可以由条件 \(3\) 和那些段决定,

所以我们其实只需要考虑条件 \(2\)


发现这就是扫描线的常见操作,我们移项就可以发现其实就是要求 \(\max - \min -r+l-k \le 0\)

于是直接用单调栈预处理一下,维护区间的最小值就可以了,这样的操作是平凡的(相信经过 lxl 这么多天的熏陶早就熟记于心)。

这道题就做完了,代码是好写的。

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=2e5+5,inf=2e9;
int n,k,d,lst,a[N],c[N],st[N],tp=0,mn;
struct node{int l,r,v;};
struct Ans{
  int l,len;
  bool operator <(const Ans &rhs) const{return (rhs.len==len)?l<rhs.l:len>rhs.len;}
}ans;
vector<node> g[N];
map<int,int> mp;

struct SGT{
  struct sgt{int v,tag;}tr[N<<2];
  
  #define mid ((l+r)>>1)
  #define lc p<<1
  #define rc p<<1|1
  #define lson l,mid,lc
  #define rson mid+1,r,rc
  
  void pu(int p){tr[p].v=min(tr[lc].v,tr[rc].v);}
  
  void change(int p,int v){tr[p].tag+=v,tr[p].v+=v;}
  
  void pd(int p){
  	if(!tr[p].tag) return ;
  	change(lc,tr[p].tag);change(rc,tr[p].tag);
  	tr[p].tag=0;
  }
  
  void build(int l=1,int r=n,int p=1){
  	if(l==r) return tr[p].tag=0,tr[p].v=l-k,void();
  	build(lson);build(rson);pu(p);
  }
  
  void upd(int x,int y,int v,int l=1,int r=n,int p=1){
  	if(x<=l&&y>=r) return change(p,v);pd(p);
  	if(x<=mid) upd(x,y,v,lson);
  	if(y>mid) upd(x,y,v,rson);pu(p);
  }
  
  int qry(int x,int y,int l=1,int r=n,int p=1){
  	if(x>r||y<l||tr[p].v>0) return -1;
  	if(l==r) return l;
  	pd(p);
  	if(x<=l&&y>=r){
  	  if(tr[lc].v<=0) return qry(x,y,lson);
  	  return qry(x,y,rson);
  	}
  	int res=qry(x,y,lson);
  	if(res!=-1) return res;
  	return qry(x,y,rson);
  }
}T;

void sol(int l,int r){
  tp=0;st[tp]=0;
  for(int i=l;i<=r;i++){
  	while(tp&&a[i]>a[st[tp]]){
  	  g[st[tp]].pb((node){st[tp-1]+1,st[tp],a[st[tp]]});
  	  g[i].pb((node){st[tp-1]+1,st[tp],-a[st[tp]]});
  	  --tp;
  	}
  	st[++tp]=i;
  }
  while(tp) g[st[tp]].pb((node){st[tp-1]+1,st[tp],a[st[tp]]}),--tp;
  tp=0;st[tp]=0;
  for(int i=l;i<=r;i++){
  	while(tp&&a[i]<a[st[tp]]){
  	  g[st[tp]].pb((node){st[tp-1]+1,st[tp],-a[st[tp]]});
  	  g[i].pb((node){st[tp-1]+1,st[tp],a[st[tp]]});
  	  --tp;	
  	}
  	st[++tp]=i;
  }
  while(tp) g[st[tp]].pb((node){st[tp-1]+1,st[tp],-a[st[tp]]}),--tp;
}

void init(){
  if(mn<0) for(int i=1;i<=n;i++) a[i]-=(mn-1);
  for(int i=1;i<=n;i++) c[i]=a[i]%d,a[i]/=d,g[i].pb((node){1,n,-i}),g[i+1].pb((node){1,n,i});
  lst=1;
  for(int i=2;i<=n;i++) 
    if(c[i]!=c[i-1]) sol(lst,i-1),lst=i;
  sol(lst,n);
}

int main(){
  /*2023.12.19 H_W_Y CF407E k-d-sequence 扫描线*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>k>>d;lst=0;ans=(Ans){0,0};mn=inf;
  for(int i=1;i<=n;i++) cin>>a[i],mn=min(a[i],mn);
  if(d==0){
  	lst=1;
  	for(int i=2;i<=n;i++){
  	  cin>>a[i];
  	  if(a[i]!=a[i-1]) ans=min(ans,(Ans){lst,i-lst}),lst=i;
  	}
  	ans=min(ans,(Ans){lst,n-lst+1});
  	cout<<ans.l<<' '<<(ans.l+ans.len-1)<<'\n';
  	return 0;
  }
  init();T.build();lst=0;
  for(int i=1;i<=n;i++){
  	for(auto j:g[i]) T.upd(j.l,j.r,j.v);
  	if(mp.count(a[i])) lst=max(lst,mp[a[i]]);
  	mp[a[i]]=i;
  	if(c[i]!=c[i-1]) lst=max(lst,i-1);
  	int nw=T.qry(lst+1,i);
  	ans=min(ans,(Ans){nw,i-nw+1});
  }
  cout<<ans.l<<' '<<(ans.l+ans.len-1)<<'\n';
  return 0;
}

P5428 [USACO19OPEN] Cow Steeplechase II S - 计算几何中的 ds

P5428 [USACO19OPEN] Cow Steeplechase II S

在过去,Farmer John曾经构思了许多新式奶牛运动项目的点子,其中就包括奶牛障碍赛,是奶牛们在赛道上跑越障碍栏架的竞速项目。他之前对推广这项运动做出的努力结果喜忧参半,所以他希望在他的农场上建造一个更大的奶牛障碍赛的场地,试着让这项运动更加普及。

Farmer John为新场地精心设计了 $ N $ 个障碍栏架,编号为 $ 1 \ldots
N $ ( $ 2 \leq N \leq 10^5 $ ),每一个栏架都可以用这一场地的二维地图中的一条线段来表示。这些线段本应两两不相交,包括端点位置。

不幸的是,Farmer John在绘制场地地图的时候不够仔细,现在发现线段之间出现了交点。然而,他同时注意到只要移除一条线段,这张地图就可以恢复到预期没有相交线段的状态(包括端点位置)。

请求出Farmer John为了恢复没有线段相交这一属性所需要从他的计划中删去的一条线段。如果有多条线段移除后均可满足条件,请输出在输入中出现最早的线段的序号。

很明显就是需要找到一组相交的线段,然后 \(\mathcal O(n)\) 判断一下那条线段是答案就可以了。


而如果找到一条相交的线段呢?

发现我们可以用类似于扫描线的方法从左到右依次扫过每一个端点,

用一个 set 维护一下已经有的线段集合,他们两两是不交的。


容易发现,如果整个集合中有交,那么第一个交点一定是相邻的两条线段,

这里令相邻就是当 \(x\) 是当前的这个地方时他们的 \(y\) 值。

于是加入和删除的时候我们只需要判断一下和相邻的两条线段是否相交即可。


而线段是否相交直接用叉积判断即可。

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define db double

const int N=2e5+5;
int n,cnt=0,fi,se;
ll nw;
struct pnt{
  ll x,y;
  int id;
  bool operator <(const pnt &a)const{return (x==a.x)?y<a.y:x<a.x;}
  pnt operator -(const pnt &a)const{return (pnt){x-a.x,y-a.y};}
}p[N];
struct lne{
  pnt l,r;
  int id;
  db calc() const{
  	if(l.x==r.x) return l.y;
    return 1.0*l.y+1.0*(r.y-l.y)*(nw-l.x)/(r.x-l.x);
  }
  bool operator <(const lne &a)const{return calc()<a.calc();}
}l[N];
set<lne> s;

ll cro(pnt a,pnt b,pnt c){
  pnt A=a-b,B=a-c;
  return A.x*B.y-A.y*B.x;
}

bool Cross(lne a,lne b){
  return min(a.l.x,a.r.x)<max(b.l.x,b.r.x)&&min(b.l.x,b.r.x)<max(a.l.x,a.r.x)&&min(a.l.y,a.r.y)<max(b.l.y,b.r.y)&&min(b.l.y,b.r.y)<max(a.l.y,a.r.y)&&cro(a.l,a.r,b.l)*cro(a.l,a.r,b.r)<0&&cro(b.l,b.r,a.l)*cro(b.l,b.r,a.r)<0;
}

int main(){
  /*2023.12.20 H_W_Y P5428 [USACO19OPEN] Cow Steeplechase II S 计算几何*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;fi=se=0;
  for(int i=1;i<=n;i++){
  	++cnt;cin>>p[cnt].x>>p[cnt].y;p[cnt].id=i;
  	++cnt;cin>>p[cnt].x>>p[cnt].y;p[cnt].id=i;
  	l[i]=(lne){p[cnt-1],p[cnt],i};
  }
  sort(p+1,p+cnt+1);
  for(int i=1;i<=cnt;i++){
  	nw=p[i].x;
  	int id=p[i].id;
  	auto seg=s.find(l[id]);
  	if(seg!=s.end()){
  	  auto pre=seg,nxt=seg;++nxt;
  	  if(pre!=s.begin()&&nxt!=s.end()){
  	  	--pre;
  	  	if(Cross(*pre,*nxt)){fi=pre->id,se=nxt->id;break;}
  	  }
  	  s.erase(seg);
  	}else{
  	  auto pos=s.lower_bound(l[id]);
  	  if(pos!=s.end()&&Cross(*pos,l[id])){fi=pos->id,se=id;break;}
  	  if(pos!=s.begin()){
  	  	--pos;
  	  	if(Cross(*pos,l[id])){fi=pos->id,se=id;break;}
  	  }
  	  s.insert(l[id]);
  	}
  }
  if(fi>se) swap(fi,se);
  int tmp=0;
  for(int i=1;i<=n;i++) if(i!=se&&Cross(l[se],l[i])) tmp++;
  cout<<(tmp>1?se:fi)<<endl;
  return 0;
  
}

Conclusion

  1. 树上面有关距离的支配点对问题,分析时找三个点把关系列出来,一般都是去找前驱和后继。(P9058 [Ynoi2004] rpmtdq)
  2. 最好不要用 KDT 乱搞,这样挺慢的,但是数据随机还是可以的。(P4475 巧克力王国)
  3. 当你觉得什么都没有问题的时候,就该问问自己 \(rt\)\(rc\) 写反没有了,或者是手滑写错了。(P3710 方方方的数据结构)

Day 7 于 2023.12.20 9:44 am. 补完。


Conclusion

线段树 + 平衡树:动态一维问题

\(\to\)(如何变强?)一维扫描线 \(\to\) 静态二维问题(换维扫描线)\(\leftarrow\) 莫队 \(\leftarrow\) 复杂统计问题

\(\to\) 升维 \(\to\) 树套树 + cdq 分治(单点修改 + 矩形查询,标记永久化

​ KDT(正统的线段树升维,标记下放

\(\to\) 树上 \(\to\) 树链剖分

常见三种扫描线:

扫描线:一维

莫队:二维

树上启发式合并:子树莫队

分治:

序列:中点分治

树:树分治

可持久化 \(\to\) 把扫描线变成在线

分块:二维(矩阵乘法规约)- 与 KDT有相似性

根号分治

静态三维问题:扫一维,DS 二维(KDT)

​ 扫二维,DS 一维(莫队)

![1](E:\H_W_Y\0-C++#代码编程\cdqz\12.8-lxl DS Day7\1.png)


ex1 计算几何中的数据结构

色彩 OI 必做题目。。。


CF1446F Line Distance - 直线与圆相交?

CF1446F Line Distance

给定一个整数 \(k\)\(n\) 个在欧几里得平面上的不同点,第 \(i\) 个点的坐标是 \((x_i,y_i)\)

考虑 \(\frac{n(n-1)}{2}\) 对坐标 \(((x_i,y_i),(x_j,y_j))\) \((1 \leq i < j \leq n)\)。对于每对坐标,计算从这两个点连成的直线到原点 \((0,0)\) 的最近直线距离。输出第 \(k\) 小的距离。

\(2 \leq n \leq 10^5, 1 \leq k \leq \frac{n(n-1)}{2}\)

首先发现可以二分答案,

于是问题就变成了求有多少条直线到原点的距离 \(\le r\)

以原点为圆心画一个半径为 \(r\) 的圆,那么我们就需要求有多少条直线与这个圆无交。


对于每一个点,我们作该点关于圆的两条切线,那么就可以得到圆上面的一段弧,

一条直线与圆无交当且仅当这两个点投射在圆上的两段圆弧相交且不包含。(画一下图,一共有三种情况)


而一条圆弧翻转之后对答案不影响,

所以我们只需要将两个弧度排序之后用树状数组维护即可。

时间复杂度 \(O(n \log^2 n)\)

Code
#include <bits/stdc++.h>
using namespace std;
#define ld long double
#define pll pair<ld,ld>
#define fi first
#define se second
#define pb push_back
#define ll long long

const ld eps=1e-9,pi=acos(-1);
const int N=2e5+5;
int a[N],b[N],n,len=0;
ld t[N],ans;
ll num=0,k;
vector<pll> g;

int id(ld x){return lower_bound(t+1,t+len+1,x)-t;}

namespace BIT{
  int tr[N];
  int lowbit(int i){return i&(-i);}
  void init(){for(int i=0;i<=len;i++) tr[i]=0;}
  void upd(int x){for(int i=x;i<=len;i+=lowbit(i)) ++tr[i];}
  int qry(int x){int res=0;for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];return res;}
}
using namespace BIT;

bool chk(ld r){
  g.resize(0);init();len=0;num=0;
  for(int i=1;i<=n;i++){
  	ld x=a[i],y=b[i],d=sqrt(x*x+y*y);
  	if(d<r+eps) continue;
  	ld org=atan2(y,x),delta=acos(r/d);
  	ld tl=org-delta,tr=org+delta;
  	if(tl<-pi) tl+=2.0*pi;
  	if(tr>pi) tr-=2.0*pi;
  	if(tl>tr) swap(tl,tr);
  	t[++len]=tl;t[++len]=tr;
  	g.pb({tl,tr});
  }
  sort(t+1,t+len+1);
  len=unique(t+1,t+len+1)-t-1;
  sort(g.begin(),g.end());
  num=1ll*n*(n-1)/2;
  for(auto i:g){
  	int L=id(i.fi),R=id(i.se);
  	num-=qry(R)-qry(L-1);upd(R);
  }
  return num>=k;
}

int main(){
  /*2023.12.13 H_W_Y CF1446F Line Distance BIT*/
  scanf("%d%lld",&n,&k);
  for(int i=1;i<=n;i++) scanf("%d%d",&a[i],&b[i]);
  ld l=0,r=2e4;
  while(r-l>eps){
  	ld mid=(l+r)/2;
    if(chk(mid)) r=mid;
    else l=mid;
  }
  printf("%.9Lf\n",l);
  return 0;
}

Conclusion

  1. 判断一条直线和一个圆是否有交,可以用直线上两点对圆作切线,从而得到两段圆弧,与圆无交当且仅当这两段圆弧 相交且不包含。(CF1446F Line Distance)

posted @ 2025-07-17 12:56  H_W_Y  阅读(42)  评论(0)    收藏  举报