In mathematics you don't understand things, you just get used to them.

标签 data structures 下的文章

不太了解这个东西的具体定义是什么,总之应该是一个用数据结构维护 DP 状态的某几个维度的 trick 吧。

事实上你可以把这篇 post 理解为三个题的解集。

先直接来看 noi2020 - Destiny 这个题。

给定一棵树 $T = (V, E)$ 和点对集合 $\mathcal Q \subseteq V \times V$ ,满足对于所有 $(u, v) \in \mathcal Q$,都有 $u \neq v$,并且 $u$ 是 $v$ 在树 $T$ 上的祖先。其中 $V$ 和 $E$ 分别代表树 $T$ 的结点集和边集。求有多少个不同的函数 $f$ : $E \to \{0, 1\}$(将每条边 $e \in E$ 的 $f(e)$ 值置为 $0$ 或 $1$),满足对于任何 $(u, v) \in \mathcal Q$,都存在 $u$ 到 $v$ 路径上的一条边 $e$ 使得 $f(e) = 1$。由于答案可能非常大,你只需要输出结果对 $998,244,353$(一个素数)取模的结果。

我们略过 DP 的过程,直接给出其定义 $f(x,j)$ 表示考虑子树 $i$,限制条件的 $v\in{\rm subtree}(x)$ 且限制 $(u,v)$ 尚未被满足,$u$ 的深度最深且 $j={\rm dep}(u)$ 的不同映射 $f:E_{{\rm subtree}(x)}\rightarrow\{0,1\}$ 数量,以及其转移$f(x,i)= f(x,i)\sum\limits_{y\in{\rm suf}(x)}\left(\sum\limits_{j=0}^{{\rm dep}(x)}f(y,j)+\sum\limits_{j=0}^{i}f(y,j)\right)+f(y,i)\sum\limits_{j=0}^{i-1}f(x,j)$。

令 $g(x)$ 为 $f(i)$ 的前缀和,得到平方级算法。

如果我们考虑把 DP 的第二维度状态放到线段树上,那么子树的合并就可以放到线段树上去做,即使用线段树的合并 trick 来做 DP。

我们来看看合并的具体过程。贴近实现,我们令 ${\rm merge}:\{(x,y,l,r,s_x,s_y)\}\rightarrow{\rm node}$ 表示线段树合并的过程,其中 $s_x$ & $s_y$ 表示 DP 的前缀和(即 $g$),在实现(以及下文的讲解)中,均把这两个变量视作别名。

有这样几种情形需要探讨。

  • $l=r$:此时应该把 $f(y,l)$ 合并到 $f(x,l)$ 中,直接对线段树结点维护的 DP 值进行修改;
  • $x=\Omega$:此时 $f(x)$ 的 DP 值并没有意义,在本题中可以视作零。于是打乘法标记即可;
  • $y=\Omega$:与上一条类似。

于是得到解决,参考实现。

再来看 pkuwc2018 - Minimax 这个题。

给出一棵有 $n$ 的结点的二叉有根树,并给出其叶子结点的权值,对于一个非叶子结点,其权值有 $p_i$ 概率取得儿子中的最大值, $1-p_i$ 的概率取得最小值。

令 $\{v_{i}\}$ 表示最终根结点($1$-th 结点)的所有可能取值(升序排列),其个数记为 $m$,每一个 $v_i$ 取得的概率记为 $r_i$,将其按照 ${\rm hash}(i)=i\times v_i\times r_i$ 的规则求出 $\sum_i{\rm hash}(i)$。

与上一题类(完 全)似(一 致)地,同样略过 DP 的过程,给出其定义 $f(i,j)$ 表示结点 $i$ 取得值 $j$ 的概率,以及其转移 $f(i,j)=\sum\limits_{v\in{\rm suf}(i)} f(v,j)\left(p_i\sum\limits_{1\leqslant k<j}f(v',k)+(1-p_i)\sum\limits_{j<k}f(v',k)\right)$,其中 $v'$ 表示 ${\rm suf}(i)\setminus\{v\}$ 中的唯一取值。

令 $g(i)$ 为 $f(i)$ 的前缀和(显然这里的 $\infty$ 并不是「真正的无限」),得到平方级的算法。

同样考虑把 DP 的第二维度状态放到线段树上,在线段树合并时维护 $s_x$ & $s_y$ 表示 DP 的前缀和,直接维护即可。

参考实现。

最后来看到 codeforces - 671D / Roads in Yusland

给定一棵 $n$ 个点的以 $1$ 为根的树。有 $m$ 条路径 $(x,y)$,保证 $y$ 是 $x$ 或 $x$ 的祖先,每条路径有一个权值。你要在这些路径中选择若干条路径,使它们能覆盖每条边,同时权值和最小。

这题的平方 DP 都有一定难度……看了 duyi 的题解挺久才理解……不过如果做过 noi2020 - Destiny 的话应该会好很多。

称 ${\rm route}(u,v)$ 中的 $u$ 为起点,$v$ 为终点,在 $v$ 决策一个 route 是否被选择。参考 noi2020 - Destiny 的状态设计,令 $f(x,i)$ 表示在 ${\rm subtree}(x)$ 中选择了若干 routes,其中终点深度最深并且高于 ${\rm dep}(x)$ 的深度为 $i$ 的最优方案。

转移即 $f(x,i)=\min\limits_{y\in{\rm suf}(x)}\{f(y,i)+\sum\limits_{z\in{\rm suf}(x)\setminus\{y\}}g(z)\}$,其中 $g(x)=\min\limits_{1\leqslant i<{\rm dep}(x)}\{f(x,i)\}$,还需要考虑每条 route 带来的额外转移,转移式显然不赘。这些 transforming formulae 也许带有一些构造成分在里面,至少我觉得不太自然……

然后考虑线段树合并优化,你需要支持区间增量 & 全局查询最小值 & 合并(与此同时区间取 $\min$)& 单点取 $\min$。因为 $O((n+m)\log_2n)$ 的空间复杂度并不能通过此题。

考虑以时间换空间,我们采用权值平衡树 & 启发式合并来解决(参考实现中给出的是 std::set)。

平衡树上每个结点维护一个 2-tuple $(i,f(x,i))$,以第一关键字排序。我们依次考虑如何支持上文的操作。

  • 区间增量:如果及时把 $i>{\rm dep}(x)$ 的元素弹出,这就变成了全局增量,直接维护即可;
  • 全局查询最小值:这个是本题最妙的地方,因为关键字的选取,平衡树无法简单地取出最小值,我们需要更多的性质。注意到 $\forall j<k,s.t.f(x,j)<f(x,k)$,此时的 $k$ 都是无用的,可以直接删除,这得出了此题的单调性。如此全局最小值就是平衡树上最右边的结点;
  • 合并:启发式合并即可;
  • 单点取 $\min$:相当于插入操作,直接来即可,需要注意一些不是特别细的细节(雾)。

如此时间复杂度退成了 $O((n+m)\log_2^2n)$,但是空间复杂度优化到了 $O(n+m)$,可以通过此题。

参考实现。

E

倒流一下,然后把负权边置零后跑 MST 即可。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int N=2e5+5,M=2e5+5;
struct edge
{
    int u,v;
    long long w;
    edge(int U=0,int V=0,int W=0)
    {
        u=U;
        v=V;
        w=W;
    }
}tur[M];
int n,m,fa[N];
bool cmp(edge one,edge ano)
{
    return one.w<ano.w;
}
int find(int u)
{
    if(u^fa[u])    fa[u]=find(fa[u]);
    return fa[u];
}
long long spannin()
{
    long long res=0;
    for(int i=1;i<=m;++i)
    {
        int u=tur[i].u,v=tur[i].v;
        long long w=tur[i].w;
        int x=find(u),y=find(v);
        if(x^y)
        {
            fa[x]=y;
            res+=w;
        }
    }
    return res;
}
int main()
{
    scanf("%d%d",&n,&m);
    long long sum=0;
    for(int i=1;i<=n;++i)    fa[i]=i;
    for(int i=1,u,v;i<=m;++i)
    {
        scanf("%d%d%lld",&u,&v,&tur[i].w);
    tur[i].w=max(tur[i].w,0LL);
        tur[i]=edge(u,v,tur[i].w);
        sum+=tur[i].w;
    }
    sort(tur+1,tur+1+m,cmp);
    printf("%lld\n",sum-spannin());
    return 0;
}

F

跑出原图的最短路树,非树边删除不需要考虑,树边就重新跑一次最短路(规模 $\Theta(n)$)。

类似的题(原题)有 JOI 2020 Final Olympic Bus。

#include<bits/stdc++.h>
int n,m,hd[500],ecnt,ont[319300],pre[500],ide[500];
struct edge {
  int nxt,to;
}e[319300];
void add_edge( int x,int y ) { e[++ecnt]={hd[x],y},hd[x]=ecnt; }
int dis[500],vis[500];
std::priority_queue<std::pair<int,int>,std::vector<std::pair<int,int>>,std::greater<std::pair<int,int>>> pq;
int find( int X ) {
  std::memset( dis+1,0x3f,n<<2 );
  std::memset( vis+1,0,n<<2 );
  for( dis[1]=0,pq.emplace( 0,1 ); pq.size(); ) {
    int x=pq.top().second; pq.pop();
    if( vis[x] ) continue; vis[x]=1;
    for( int i=hd[x]; i; i=e[i].nxt ) {
      if( i==X ) continue;
      int y=e[i].to;
      if( dis[y]>dis[x]+1 ) {
        dis[y]=dis[x]+1;
        pre[y]=x;
        ide[y]=i;
        if( !vis[y] ) pq.emplace( dis[y],y );
      }
    }
  }
  return dis[n]==0x3f3f3f3f?-1:dis[n];
}
signed main() {
  scanf( "%d%d",&n,&m );
  for( int i=1,x,y; i<=m; ++i ) scanf( "%d%d",&x,&y ),add_edge( x,y );
  int ret=find( -1 );
  if( ret==-1 ) {
    for( int i=1; i<=m; ++i ) puts( "-1" );
    return 0;
  }
  for( int now=n; now!=1; ) ont[ide[now]]=1,now=pre[now];
  for( int i=1; i<=m; ++i ) {
    if( ont[i] ) printf( "%d\n",find( i ) );
    else printf( "%d\n",ret );
  }
  return 0;
}

G

其实五分钟能冲出来的……去了板子码长 800B。

跑出每个结点到根的中位数,这个随便拿个数据结构维护下就行了,然后做个树 DP,就考虑深度奇偶性,和 P7443 的那个 SG 函数一个套路。注意是自底向上跑的。

#include<bits/stdc++.h>
struct btree {
  int ch[100100][2],fa[100100],val[100100],cnt[100100],sz[100100],tot,root;
  btree() { ins( std::numeric_limits<int>::max() ),ins( std::numeric_limits<int>::min() ); }
  bool wis( int p ) { return ch[fa[p]][1]==p; }
  void pull( int p ) { sz[p]=sz[ch[p][0]]+sz[ch[p][1]]+cnt[p]; }
  int dot( int v ) { return val[++tot]=v,sz[tot]=cnt[tot]=1,tot; }
  void link( int f,int p,int k ) { ch[f][k]=p,fa[p]=f; }
  void rot( int p ) {
    int f=fa[p],gf=fa[fa[p]],k=wis( p );
    link( gf,p,wis( f ) ),link( f,ch[p][k^1],k ),link( p,f,k^1 );
    pull( f ),pull( p );
  }
  void splay( int p,int t ) {
    for( ; fa[p]!=t; rot( p ) )
      if( fa[fa[p]]!=t ) rot( wis( p )!=wis( fa[p] )?p:fa[p] );
    if( t==0 ) root=p;
  }
  int find( int v ) {
    int p=root;
    if( !p ) return 0;
    while( ch[p][v>val[p]] && v!=val[p] ) p=ch[p][v>val[p]];
    return splay( p,0 ),p;
  }
  void ins( int v ) {
    int p=root,f=0;
    while( p && v!=val[p] ) f=p,p=ch[p][v>val[p]];
    if( p ) cnt[p]++;
    else fa[p=dot(v)]=f,( f && ( ch[f][v>val[f]]=p ) );
    splay( p,0 );
  }
  int boundary( int v,int f ) { // f: 0 - pre, 1 - suf
    int p=find( v );
    if( ( val[p]>v && f ) || ( val[p]<v && !f ) ) return p;
    p=ch[p][f];
    while( ch[p][f^1] ) p=ch[p][f^1];
    return p;
  }
  void del( int v ) {
    int pre=boundary( v,0 ),suf=boundary( v,1 );
    splay( pre,0 ),splay( suf,pre );
    if( cnt[ch[suf][0]]>1 ) cnt[ch[suf][0]]--,pull( suf ),splay( ch[suf][0],0 );
    else ch[suf][0]=0,pull( suf ),splay( suf,0 );
  }
  int id( int i ) {
    int p=root; i++;
    if( sz[p]<i ) return -1;
    while( 233 ) {
      if( sz[ch[p][0]]+cnt[p]<i ) i-=sz[ch[p][0]]+cnt[p],p=ch[p][1];
      else if( sz[ch[p][0]]<i ) return val[p];
      else p=ch[p][0];
    }
    return -1;
  }
}tr;
int n,a[100100],med[100100],dp[100100];
std::vector<int> g[100100];
void dfs( int x,int f,int d ) {
  tr.ins( a[x] );
  if( d&1 ) med[x]=tr.id( ( d+1 )/2 );
  else med[x]=( tr.id( d/2 )+tr.id( d/2+1 ) )/2;
  for( int y:g[x] ) if( y!=f ) dfs( y,x,d+1 );
  tr.del( a[x] );
}
void DP( int x,int f,int d ) {
  int leaf=1,mx=0,mn=1e9;
  for( int y:g[x] ) if( y!=f && ( leaf=0 )^1 )
    DP( y,x,d+1 ),mx=std::max( mx,dp[y] ),mn=std::min( mn,dp[y] );
  if( leaf ) dp[x]=med[x];
  else dp[x]=( d&1 )?mx:mn;
}
signed main() {
  scanf( "%d",&n ); for( int i=1; i<=n; ++i ) scanf( "%d",&a[i] );
  for( int i=1,x,y; i<n; ++i ) scanf( "%d%d",&x,&y ),g[x].emplace_back( y ),g[y].emplace_back( x );
  dfs( 1,0,1 ),DP( 1,0,1 ),printf( "%d\n",dp[1] );
  return 0;
}

H 不会 wqs / 反悔贪心,改天学下再说吧……

是不是有点水了呀……

没想到吧。

其实本文旨在以 object oriented 的方式工程化地描述线段树的抽象结构,大概是在翻译 ACL 的 lazysegtree

在线段树上的结点中,有两种信息,分别称为 SF,一个是维护的信息,一个是懒惰标记,又对应两个「空值」e()id()

线段树依赖于两个儿子的值是 S,依赖于父亲的值是 F,定义一个函数 op(x, y),其中 $x,y\in\mathbb{S}$ 表示 $x,y$ 合并的结果,这个规则是自定义的,且需要满足结合律和交换律;以及 composition(x, y),其中 $x,y\in\mathbb{F}$,表示把懒惰标记 $x$ 单向传递到懒惰标记 $y$ 上后的结果,其规则同样是自定义的。如果不理解为什么是单向请参考 Range Affine, Range Sum 问题。

类似于 composition(x, y) 使得可以让一个在 $\mathbb{F}$ 中的元素传递到另一个属于 $\mathbb{F}$ 的元素身上,我们有 mapping(x, y) 表示从 $\mathbb{F}$ 到 $\mathbb{S}$ 的映射,其中 $x\in\mathbb{F}$,$y\in\mathbb{S}$。

其实可以发现所谓的 S 就是一类幺半群(monoid),注意 F 显然不具有此类性质,但是 Fcomposition(x, y) 是有封闭性的。

例题 P3373,示例代码见此处,特征码 acl-k。

闲话,如果你的代码实现真的分这么细码量会激增哦。(

其实这个的标题叫 平凡线段树上二分幻术,因为这是一个民科在乱叫。

如标题所言,这个东西确实非常 trivial。碍于网络上没有一个成体系的文章供参考就只能自己来炒炒冷饭。

如果出了什么 bug 就当个笑话看。


我们这样来描述一类问题

给出一个序列 $\{a_n\}$ 以及函数 $\textbf{f}(x)\in\{0,1\}$,在 $[l,r]$ 中查找满足 $\textbf{f}(a_i)=1$ 的所有位置所组成的集合中的一个元素,该元素满足某个指定的性质。

网络上大部分资料对这类问题的线段树二分做法只停留在全局查询。事实上,线段树二分的做法完全可以搬到区间上,做法并不困难。

以下令 $[l,r]$ 为线段树执行时的当前区间,$[x,y]$ 为询问区间。

我们在执行线段树的时候,维护一个标记 $g\in\{0,1\}=[[l,r]\subseteq[x,y]]$,如果为 $0$,则继续在线段树上搜寻询问区间,否则就执行二分,因为查询的结点数为 $\Theta(\log_2 n)$ 且树的深度为 $\Theta(\log_2 n)$,所以单次复杂度 $\Theta(\log_2 n)$。


例题大概是 codeforces - 407E,我负责给李涵口胡,然后李涵就把代码杠出来了。

link。

首先对原序列排序,考虑静态序列做法为:设 $f(n,k\in\{0,1\})$ 为对于前 $n$ 个数,第 $n$ 个数否 / 是已经决策完毕的最优方案,转移即

$$ \begin{cases} f(n,0)=f(n-1,1) \\ \displaystyle f(n,1)=\lvert a(n)-a(n-1)\rvert+\min\{f(n-1,\{0,1\})\} \end{cases} $$

再由于增 / 减数操作,我们使用数据结构来维护,如果使用维护区间的数据结构那么区间 DP 是最好的选择。把状态换为 $f(l,r,k_1\in\{0,1\},k_2\in\{0,1\})$ 表示考虑区间 $[l,r]$,左 / 右端点分别否 / 是已经决策完毕。转移即

$$ f(l,r,k_1,k_2)=\min_{a+b>1}\{f(l,m,k_1,a),f(m,r,b,k_2)\} $$

注意使用不同的数据结构转移会有不同,此处为平衡树情况的转移。不过代码写的是线段树,请注意区分。$m$ 为 $[l,r]$ 中一点,也需要注意 $r-l\leqslant2$ 情况的特判。

省去了一些东西,完整代码见此处,特征码 cc-strquer

const long long inf = 1e18;
namespace 😅😅 {
struct node {
  int ls, rs, num;
  long long l, r, mx, mn;
  long long dp[2][2];
  long long *const operator[](const long long i) { return dp[i]; }
} 😅[2 * 200000 * 60];
int tot;
int fuck😅(long long l, long long r) {
  int shit = ++tot;
  😅[shit].ls = 😅[shit].rs = 0;
  😅[shit].l = l, 😅[shit].r = r;
  😅[shit].num = 0;
  return shit;
}
void Merge(node &$😅, node x, node y) {
  node ap;
  bool flag = 0;
  if (!x.num) {
    ap = y;
    flag = 1;
  }
  if (!y.num) {
    ap = x;
    flag = 1;
  }
  if (flag) {
    $😅.mn = ap.mn;
    $😅.mx = ap.mx;
    for (int i = 0; i < 2; ++i) {
      for (int j = 0; j < 2; ++j) $😅[i][j] = ap[i][j];
    }
    return;
  }
  $😅.mn = x.mn;
  $😅.mx = y.mx;
  for (int i = 0; i < 2; ++i) {
    for (int j = 0; j < 2; ++j) {
      $😅[i][j] = -inf;
      for (int a = 0; a < 2; ++a) {
        for (int b = 0; b < 2; ++b)
          Max($😅[i][j], x[i][a] + y[b][j] + a * b * (y.mn - x.mx));
      }
    }
  }
}
void add(int p, long long x, int v) {
  auto &l = 😅[p].l, &r = 😅[p].r;
  if (!p) p = ++tot;
  😅[p].num += v;
  if (l == r) {
    😅[p].mx = 😅[p].mn = x;
    😅[p][0][0] = 😅[p][1][1] = -inf;
    😅[p][0][1] = 😅[p][1][0] = 0;
    return;
  }
  long long mid = (l + r) >> 1;
  if (mid >= x) {
    if (!😅[p].ls) 😅[p].ls = fuck😅(l, mid);
    add(😅[p].ls, x, v);
  } else {
    if (!😅[p].rs) 😅[p].rs = fuck😅(mid + 1, r);
    add(😅[p].rs, x, v);
  }
  Merge(😅[p], 😅[😅[p].ls], 😅[😅[p].rs]);
}
}  // namespace 😅😅
signed main() {
  int T;
  for (cin > T; T; --T) {
    int n, q;
    cin > n > q;
    😅😅::fuck😅(0, inf);
    for (long long x; n; --n) {
      cin > x;
      😅😅::add(1, x, 1);
    }
    for (int t; q; --q) {
      long long x;
      cin > t > x;
      if (t == 0)
        😅😅::add(1, x, 1);
      else
        😅😅::add(1, x, -1);
      cout < 😅😅::😅[1].mx - 😅😅::😅[1].mn - imax(😅😅::😅[1][1][1], 0LL) < '\n';
    }
    😅😅::tot = 0;
  }
  return 0;
}