【搬】板子整理计划

0 前言

由于有一些板子过长,我就丢例题里了,剩下的不一定会单独贴出来一份。

1.数据结构

1.1 动态开点线段树

众所周知,有些题目十分恶心,值域非常大但是数的个数却勉强可以存。

此时有两个方法。

第一个方法,离散化

谁好端端地想敲动态开点啊。

虽然离散化也很不简单,正好整理一下。

去重,排序,最后二分出每一个数在其中的位置,将其编号对应起来。

其实就是哈希,贴一个以前写过题目用的。


sort( a + 1, a + N + 1 );
int len = unique( a + 1, a + N + 1 ) - ( a + 1 );
for( int i = 1; i <= N; i ++ ){
    b[i] = lower_bound( a + 1, a + len + 1, b[i] ) - a;
}

动态开点,顾名思义,要用到的点才开。

那么我们遇到的点就顺便开一下就可以了。

也很好理解,线段树的左右儿子分别是 \(2p, 2p+1\)

但是动态开点的左右儿子需要单独存,即每个点都要额外存两份编号。

[例1.1.1](https://www.luogu.com.cn/problem/CF915E)

下面是带懒标记的板子,有点丑。

const int MAXN = 15000010;
int N, Q;
int tot, root;
struct tree{
    int ls, rs, sum, lzyt;
    //懒标记:-1表示没有,0、1均表示区间被整体赋值为0、1,分别为工作日和非工作日
}tr[MAXN];

// 动态开点中一般用不到build,因为无法确定当前在原数组中编号?
void down( int p, int l, int r ){
    if( tr[p].lzyt == -1 ) return;
    if( !tr[p].ls ){
        tr[tr[p].ls = ++ tot] = { 0, 0, 0, -1 };
    }
    if( !tr[p].rs ){
        tr[tr[p].rs = ++ tot] = { 0, 0, 0, -1 };
    }
    int mid = ( l + r ) >> 1;
    tr[tr[p].ls].sum = tr[p].lzyt * ( mid - l + 1 );
    tr[tr[p].ls].lzyt = tr[p].lzyt;
    tr[tr[p].rs].sum = tr[p].lzyt * ( r - mid );//注意右区间范围,[mid + 1, r ]
    tr[tr[p].rs].lzyt = tr[p].lzyt;
    tr[p].lzyt = -1;
}

void upd( int p ){ tr[p].sum = tr[tr[p].ls].sum + tr[tr[p].rs].sum; }

void mdf( int & p, int l, int r, int x, int y, int val ){//将区间[l, r]赋值为val
    if( ! p ) tr[p = ++ tot] = { 0, 0, r - l + 1, -1 };//默认都是
    if( x <= l && r <= y ){
        if( val == 2 ) val = 0;
        tr[p].lzyt = val, tr[p].sum = val * ( r - l + 1 );
        return;
    }
    down( p, l, r );
    int mid = ( l + r ) >> 1;
    if( x <= mid ) mdf( tr[p].ls, l, mid, x, y, val );
    if( y > mid ) mdf( tr[p].rs, mid + 1, r, x, y, val );
    upd( p );
}

signed main(){
    IOS;
    cin >> N >> Q;
    while( Q -- ){
        int l, r, k;
        cin >> l >> r >> k;
        mdf( root, 1, N, l, r, k );
        cout << N - tr[root].sum << endl;
    }
    return 0;
}

1.2 线段树合并

先说原理,又顾名思义,将两棵线段树以某种方式合并在一起便是线段树合并。

一般是权值线段树(不知原因),又一般是动态开点。

核心在 \(merge\) 函数里面,主要分两边都是空的和两边非空还有一边空的情况。

  • 两边都是空的:结束递归。
  • 一边:直接继承。
  • 两边都不是:直到叶子再按所需合并,先递归下去。
[例1.2.1](https://www.luogu.com.cn/problem/P4556)

这其实一点都不板,感觉好难。
线段树合并板子,带lca、动态开点和权值线段树。

  • 把路径操作变成点差分操作,即 \(u + 1, v + 1, lca(u, v) - 1, fa_{lca( u, v )} - 1\)
  • 将子树的桶通过线段树合并到整个树。
  • 通过动态开点的权值线段树存储桶,即对于每个房屋节点u,开一棵权值线段树存储每种救济粮出现的次数。
  • Details of merge:
    • 两个桶都没有值 -> 区间[l, r]都是空节点。
    • 一个有 -> 一个有点,那就直接继承子树(注意懒标记)。
    • 两个都有 -> 区间[l,r]都有点,先递归合并子区间再信息合并上来。
    • 不想破坏原先两个线段树,就得新开。
  • (合并操作)时间上:最坏可以到值域,可证大概是 \(O( M \cdot \log(V) )\),其中 \(V\) 为值域

一直卡在最后统计答案要第一次扫到的时候存,不然它被合并之后就错了。

const int MAXN = 6e6 + 5;
const int MAXZ = 1e5 + 5;
int N, M;
int f[MAXZ][32], dep[MAXZ];
void dfs( int u, int fa ){
    dep[u] = dep[fa] + 1;
    f[u][0] = fa;
    for( int i = 1; i <= 20; i ++ ){
        f[u][i] = f[f[u][i - 1]][i - 1];
    }   
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u );
    }
}

int lca( int x, int y ){
    if( dep[x] < dep[y] ) swap( x, y );
    for( int i = 20; i >= 0; i -- ){
        if( dep[f[x][i]] >= dep[y] ) x = f[x][i];
    }
    if( x == y ) return x;
    for( int i = 20; i >= 0; i -- ){
        if( f[x][i] != f[y][i] ) x = f[x][i], y = f[y][i];
    }
    return f[x][0];
}

int tot, root[MAXZ], ans[MAXZ];

struct TREE{
    int ls, rs, val, pos;//此处val为出现频次,pos为在叶子节点时,表示当前储存的区间(点)
}tr[MAXN];

void upd( int p ){
    if( tr[tr[p].ls].val >= tr[tr[p].rs].val ){
        tr[p].val = tr[tr[p].ls].val;
        tr[p].pos = tr[tr[p].ls].pos;
    } else{
        tr[p].val = tr[tr[p].rs].val;
        tr[p].pos = tr[tr[p].rs].pos;
    }
}

void mdf( int & p, int l, int r, int pos, int val ){
    if( !p ) p = ++ tot;
    if( l == r ){
        tr[p].val += val;
        tr[p].pos = ( tr[p].val > 0 ) ? l : 0;
        return;
    }
    int mid = ( l + r ) >> 1;
    if( pos <= mid ) mdf( tr[p].ls, l, mid, pos, val );
    else mdf( tr[p].rs, mid + 1, r, pos, val );
    upd( p );
}

int mrg( int x, int y, int l, int r ){
    if( !x ) return y;
    if( !y ) return x;
    if( l == r ){
        tr[x].val += tr[y].val;
        tr[x].pos = ( tr[x].val > 0 ) ? l : 0;
        return x;
    }
    int mid = ( l + r ) >> 1;
    tr[x].ls = mrg( tr[x].ls, tr[y].ls, l, mid );
    tr[x].rs = mrg( tr[x].rs, tr[y].rs, mid + 1, r );
    upd( x );
    return x;
}

void ddfs( int u, int fa ){
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        ddfs( v, u );
        root[u] = mrg( root[u], root[v], 1, MAXZ );
    }
    ans[u] = tr[root[u]].pos;
}

signed main(){
    cin >> N >> M;
    for( int i = 1; i < N; i ++ ){
        int u, v;
        cin >> u >> v;
        add( u, v ), add( v, u );
    }
    dfs( 1, 0 );
    while( M -- ){
        int x, y, z;
        cin >> x >> y >> z;
        int l = lca( x, y );
        mdf( root[x], 1, MAXZ, z, 1 );
        mdf( root[y], 1, MAXZ, z, 1 );
        mdf( root[l], 1, MAXZ, z, -1 );
        if( l != 1 ) mdf( root[f[l][0]], 1, MAXZ, z, -1 );
    }

    ddfs( 1, 0 );

    for( int i = 1; i <= N; i ++ ){
        cout << ans[i] << endl;
    }
    return 0;
}
[例1.2.2](https://www.luogu.com.cn/problem/P3605)

稍易一些,就不说思路了。

const int MAXN = 1e5 + 5;
const int MAXT = 6e6 + 5;
const int MAXP = 1e9 + 5;
int N;
int p[MAXN];

int root[MAXN], tot;
struct TREE{
    int ls, rs, cnt;
}tr[MAXT];

void upd( int p ){ tr[p].cnt = tr[tr[p].ls].cnt + tr[tr[p].rs].cnt; }

void mdf( int & p, int l, int r, int pos ){
    if( !p ) p = ++ tot;
    if( l == r ){
        tr[p].cnt ++;
        return;
    }
    int mid = ( l + r ) >> 1;
    if( pos <= mid ) mdf( tr[p].ls, l, mid, pos );
    else mdf( tr[p].rs, mid + 1, r, pos ); 
    upd( p );
}

int qry( int p, int l, int r, int x ){
    if( !p || r <= x ) return 0;
    if( l > x ) return tr[p].cnt;
    int mid = ( l + r ) >> 1;
    return qry( tr[p].ls, l, mid, x ) + qry( tr[p].rs, mid + 1, r, x );
}

int mrg( int x, int y, int l, int r ){
    if( !x ) return y;
    if( !y ) return x;
    if( l == r ){
        tr[x].cnt += tr[y].cnt;
        return x;
    }
    int mid = ( l + r ) >> 1;
    tr[x].ls = mrg( tr[x].ls, tr[y].ls, l, mid );
    tr[x].rs = mrg( tr[x].rs, tr[y].rs, mid + 1, r );
    upd( x );
    return x;
}


int ans[MAXN];

void dfs( int u, int fa ){
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u );
        root[u] = mrg( root[u], root[v], 1, MAXP );
    }
    ans[u] = qry( root[u], 1, MAXP, p[u] );
}

signed main(){
    cin >> N;
    for( int i = 1; i <= N; i ++ ){
        cin >> p[i];
        mdf( root[i], 1, MAXP, p[i] );
    }

    for( int i = 2; i <= N; i ++ ){
        int k;
        cin >> k;
        add( i, k ), add( k, i );
    }

    dfs( 1, 0 );
    
    for( int i = 1; i <= N; i ++ )
        cout << ans[i] << endl;
    return 0;
}
$\textup{更多例题}$ [1](https://www.luogu.com.cn/problem/P3224) | [2](https://www.luogu.com.cn/problem/P3521)

哦对还有什么垃圾回收写法,在线段树分裂里使用了,移步。


1.3 线段树分裂

有顾名思义,即线段树合并的逆运算。

在某种设定的方式下,将一棵线段树分成两棵。

给出常见操作:

  • 分出区间中的值;
  • 分出前 \(k\) 小的值;
  • 分出比 \(val\) 小的值。

Take 将区间值取出 for example,和分裂同样的,分类:

  • 当这个点和区间没有相交,直接返回;
  • 如果被完全包含,直接删除并且加入、返回;
  • 如果部分相交,新树种建该节点并递归。

正好后面这种不是区间值的( •̀ ω •́ )。

::::success[\(\textup{例1.3.1}\)]

:::warning[简化题意]

  1. \(p\) 中的区间[x, y]建新树
  2. \(t\) 中的树放到 \(p\) 里并删除
  3. \(p\) 中加入 \(x\) 个给出数字
  4. 查询 \(p\) 中[x, y]范围内的树
  5. 查询其中第 \(k\) 小的数

那么此处就要包括合并操作,单点修改操作,区间求和操作,区间第k小操作和分裂操作。真恶心。
:::

前面不过多提及了,这里点两个重点。

:::info[1.垃圾回收&好习惯]

这题中,我使用了如下写法:

void dlt( int p ){ if( p ) pool[++ potp] = p, tr[p] = { 0, 0, 0 }; }

int nwn(){ return ( potp ? pool[potp --] : ++ tot ); }

原理很简单,即使用一个队列 \(pool\) 回收不要的点编号,用的时候丢进去即可。
:::

  1. 此处分裂代码
void split( int x, int & y, ll k ){//保留前k个元素在x中,剩下放到y中
    if( !x ) return;
    y = nwn();
    if( k > tr[tr[x].ls].val )//若为左右子树各一部分,即左子树小于k,去右边并且将k修改为 $k - V_lft$
        split( tr[x].rs, tr[y].rs, k - tr[tr[x].ls].val );
    else swap( tr[x].rs, tr[y].rs );//否则全在右边,直接换
    if( k < tr[tr[x].ls].val )
        split( tr[x].ls, tr[y].ls, k );//如果大于,那么就右子树全部给,递归左边
    tr[y].val = tr[x].val - k;
    tr[x].val = k;//分配剩下的
    return;
}

感觉注释里面有点错,晚点改。

::::

跑了,以后再来加练习题。


1.4 李超线段树

线段树也有真正意义上维护线段的一天吗?有的。

不难联想到,维护线段自然会用到一次函数,即 \(y = kx + b\)

如此方便,便可以通过维护 \(k\)\(b\) 来实现维护线段。

如下面例题。

::::success[\(\textup{例1.4.1}\)]

这个博客写的很清楚。

注意到我们需要插入操作和求区间最高线段操作。

插入操作主要是要通过 \(cmp\) 去比较每个影响到的节点中哪个最优,步骤如下:

  • 判断两线段是否相交,不交直接判断高矮(无交集直接退出)

  • 若相交,且当前递归区间被线段区间完全包含,从 \(mid\) 位置再进行判断:

    树中存储的为 \(mid\) 位置更大的那个,然后再看左右两边哪边大,递归。

  • 若没被包含,则递归下去直到满足条件。

:::info[更为简洁的写法]
学姐给的,先丢这里。

bool won(int p,int q,int x){
	if(!q) return 1;
	double yp=a[p].f(x),yq=a[q].f(x);
	if(fabs(yp-yq)<=eps) return p<q;
	return yp>yq;
}
namespace seg{
	int tr[maxn<<2];
	#define mid ((l+r)>>1)
	#define ls p<<1
	#define rs p<<1|1
	void gmx(int &p,int q,int x){if(!won(p,q,x)) p=q;}
	void ins(int p,int l,int r,int ln){
		if(!tr[p]){
			tr[p]=ln;
			if(l^r) ins(ls,l,mid,ln),ins(rs,mid+1,r,ln);
			return;
		}
		bool fl=won(ln,tr[p],l),fm=won(ln,tr[p],mid);
		if(fm) swap(ln,tr[p]);
		if(l==r) return;
		if(fm==fl) ins(rs,mid+1,r,ln);
		else ins(ls,l,mid,ln);
	}
	void upd(int p,int l,int r,int ql,int qr,int ln){
		if(ql<=l&&r<=qr) return ins(p,l,r,ln);
		if(l==r) return;
		if(ql<=mid) upd(ls,l,mid,ql,qr,ln);
		if(qr>mid)	upd(rs,mid+1,r,ql,qr,ln);
	}
	int qry(int p,int l,int r,int x){
		if(x<l||r<x) return 0;
		if(l==r) return tr[p];
		int res=tr[p];
		if(x<=mid) gmx(res,qry(ls,l,mid,x),x);
		else gmx(res,qry(rs,mid+1,r,x),x);
		return res;
	}
}using namespace seg;

:::

查询操作更为直观,这里不谈。

稍有特色的还有斜率比较这里,即通过解析式计算在 \(x\) 点的坐标高度,然后比较。

注意精度问题,并且如果两个差小于某个精度,就比较编号大小(题目要求)

const int MAXN = 1e5 + 5;
const int MOD = 39989;
const int MOD1 = 1e9;
const double eps = 1e-10;//用于调整精度
int N;
struct line{
    double k, b;
}ln[MAXN];//存储每条线段的解析式

int tot1 = 0;

struct lct{
    int ls, rs, id;//左右编号,区间内优势线段的编号
}tr[MAXN << 4];

double ycalc( int i, int x ){ return ln[i].k * x + ln[i].b; }
//计算第i条线段在x处的坐标

bool cmp( int i, int j, int x ){//比较线段i、j在x处谁更高
    if( ycalc( i, x ) - ycalc( j, x ) > eps ) return true;
    if( ycalc( j, x ) - ycalc( i, x ) > eps ) return false;
    return i < j;//否则判断编号大小,返回小的那个
}

//插入线段,线段区间在[x1, x2],编号为id
void ins( int & p, int l, int r, int x1, int x2, int id ){
    if( r < x1 || l > x2 ) return ;//没有交集,不会影响树中的值
    if( !p ) p = ++ tot1;
    if( x1 <= l && r <= x2 ){
        if( cmp( id, tr[p].id, x1 ) && cmp( id, tr[p].id, x2 ) ){
            tr[p].id = id;
            return;
        } else if( ! ( cmp( id, tr[p].id, x1 ) || cmp( id, tr[p].id, x2 ) ) ) return;
        int mid = ( l + r ) >> 1;
        if( ( cmp( id, tr[p].id, mid ) ) ) swap( id, tr[p].id );
        if( ( cmp( id, tr[p].id, l ) ) ) ins( tr[p].ls, l, mid, x1, x2, id );
        else ins( tr[p].rs, mid + 1, r, x1, x2, id );
    }else{
        int mid = ( l + r ) >> 1;
        ins( tr[p].ls, l, mid, x1, x2, id );
        ins( tr[p].rs, mid + 1, r, x1, x2, id );
    }
}

int lstans = 0, root = 0, tot;
//查询在点x的线段
void qry( int p, int l, int r, int x ){
    if( !p || x < l || x > r ) return;
    if( cmp( tr[p].id, lstans, x ) ) lstans = tr[p].id;
    if( l == r ) return;
    int mid = ( l + r ) >> 1;
    if( x <= mid ) qry( tr[p].ls, l, mid, x );
    else qry( tr[p].rs, mid + 1, r, x );
}

int get( int x, int mod ){
    return ( x + lstans - 1 ) % mod + 1;
}

int main(){
    cin >> N;
    while( N -- ){
        int op, k, x, y, xx, yy;
        cin >> op;
        if( op == 0 ){
            cin >> k;
            int x = ( k + lstans - 1 ) % MOD + 1;
            lstans = 0;
            qry( root, 1, MOD, x );
            cout << lstans << endl;
        } else{
            cin >> x >> y >> xx >> yy;
            x = get( x, MOD ), y = get( y, MOD1 ), xx = get( xx, MOD ), yy = get( yy, MOD1 );
            if( x > xx ) swap( x, xx ), swap( y, yy );
            if( x != xx ){
               ln[++ tot].k = 1.0 * ( yy - y ) / ( xx - x );
               ln[tot].b = (double) y -  ln[tot].k * x;
            } else{
                ln[++ tot].k = 0;
                ln[tot].b = max( y, yy );
            }
            ins( root, 1, MOD, x, xx, tot );
        }
    }
    return 0;
}

::::


1.5 颜色段均摊/ODT/珂朵莉树

其实我都没懂这玩意和树有什么关系?

暴力数据结构,颜色体现在处理有关区间颜色覆盖问题的功能,均摊体现在时间上是以均摊呈现的。

大致操作为使用 \(set\) 维护每段左右编号以及其颜色,基于二分出左右端点。

  • 区间覆盖(assign)将区间内所有段删除,并且插入新段。此处查询等操作需要使用 \(split\) 操作。

  • 区间查询,直接二分出来查。

难点在一些高级语法的使用。

:::success[\(\textup{例1.5.1}\)]

如上操作,查询时查该段左右端点是否满足颜色数不超过 \(2\) 即可。

int N, K;
string s;
struct cth{
    int l;
    mutable int r;
    mutable char col;
    //一种很高级的写法,调用这个函数就会将默认的参数传进去
    cth( int l, int r = 0, char ch = 0 ) : l(l), r(r), col(ch){}

    bool operator < ( const cth & ct ) const {
        return l < ct.l;
    }
};

set<cth> odt;

//把包含p位置的区间分裂后返回[pos,r](迭代器)
auto split( int p ){
    if( p > N ) return odt.end();
    auto it = odt.lower_bound( cth( p ) );
    //有现成的直接返回
    if( it != odt.end() && it->l == p ) return it;
    if( it != odt.begin() ) it --;//回到前一个区间
    int l = it->l, r = it->r;
    char c = it->col;
    odt.erase( it ), odt.insert( cth( l, p - 1, c ) );
    return odt.insert( cth( p, r, c ) ).first;//insert原来是有返回值的,是pair<it, bool>
}

//其实就是区间修改,将[l, r]改成字符c,不过叫做覆盖
void assign( int l, int r, char c ){
    auto itr = split( r + 1 ), itl = split( l );
    odt.erase( itl, itr );//删除区间内的所有值
    auto it = odt.insert( cth( l, r, c ) ).first;
    if( next( it ) != odt.end() && next( it )->col == c ){
        auto rgit = next( it );
        it->r = rgit->r, odt.erase( rgit );
    }
    if( it != odt.begin() && prev( it )->col == c ){
        auto lfit = prev( it );
        lfit->r = it->r, odt.erase( it );
    }
}

//查询区间[l, r]是否满足题目中材料的要求
bool query( int l, int r ){
    auto it1 = prev(odt.upper_bound( cth( l ) ) );
    auto it2 = prev(odt.upper_bound( cth ( r ) ) );
    if( it1 != it2 ) return false;
    if( l == 1 || r == N ) return true;
    auto rgit =  prev( odt.upper_bound( cth( l - 1 ) ) ), lfit = prev(odt.upper_bound( cth( r + 1 ) ) );
    return rgit->col != lfit->col;
}

int main(){
    cin >> N;
    cin >> s;
    for( int i = 0; i < N; ){
        int j = i;
        while( j < N && s[j] == s[i] ) j ++;
        odt.insert( cth( i + 1, j, s[i] ) );
        i = j;
    }
    cin >> K;
    while( K -- ){
        char op, k;
        int x, y;
        cin >> op >> x >> y;
        if( op == 'A' ){
            cin >> k;
            assign( x, y, k );
        } else{
            cout << ( query( x, y ) ? "Yes" : "No" ) << endl;
        }
    }
    return 0;
}

:::


2.树论

2.? 树链剖分

2.?.1 重链剖分

我们定义一个树中子树最大的儿子为重儿子,考虑将树变换为线段。

剖分,顾名思义,将树结构解剖成多个线段,便于维护。

重链,即取其重儿子并且进行剖分,将最重的那条肢解出来维护。

当然,不同的重链之间会用轻边过度,即非重边的那些边。

讲一下这玩意的用处,比如区间和,直接维护十分迷茫,便拆开将重链合并起来用线段树维护,段与段之间暴力即可。

有一个很好用的性质,就是注意到如果我们用 \(dfn\) 序进行剖分,那么一段重区间的 \(dfn\) 序应当是连续的!

:::info[板子]

void dfs( int u, int fa ){
    siz[u] = 1;
    int nowmax = 0;
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u );
        siz[u] += siz[v];
        if( siz[v] > nowmax ) nowmax = siz[v], hson[u] = v;
    }
}

:::

下面是例题。

:::success[没有]
最近没怎么做。

lca、一些板子题以后贴。
:::


2.?.2 长链剖分

容易想到,长链剖分就是找最长链的儿子然后剖分,便有了名为长儿子的概念。

用途下面两个例题各占一个,请看下文:

:::success[\(\textup{例2.?.2.1}\)]

主要是优化dp,至于为什么可以优化。

因为询问和深度相关,便认定使用长链剖分。

按照题目所给出dp做会超时,发现和深度有关,考虑长链剖分。

需要兼备时间空间,便开一块公用空间buf,又因为这题转移是直接赋值。

则每次转移时选长儿子直接用指针数组f进行复制,节省时间空间。

而轻儿子直接转即可。

由于总点数不超过 N,故最后复杂度在O(N)

int lson[MAXN], dep[MAXN];//lson[u]表示节点u的长儿子编号,dep[u]表示其子树的高度
//因为找离根更远的,我们要选择到叶子距离最大的儿子!
void dfs( int u, int fa ){
    lson[u] = 0;
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u );
        if( lson[u] == 0 || dep[v] > dep[lson[u]] ) lson[u] = v;
    }
    dep[u] = dep[lson[u]] + 1;
}

int buf[MAXN], *now = buf;//内存池,用于分配f | now存储空闲的起点
int *f[MAXN];//指针,指向buf中连续区间的起始位置,但是f[u][i]表示u的子树中与u距离为i的点的个数
//所以这里通过使指针f[u]指向buf中的某个位置,并且在*(f[u] + i)位置存储f[u][i]
int ans[MAXN];//存储u的主导下标
void dfs2( int u, int fa ){
    f[u][0] = 1;//相当于*(f[u] + i)->距离为0的只有自己
    if( lson[u] ){
        f[lson[u]] = f[u] + 1;//内存共用
        dfs2( lson[u], u );
        ans[u] = ans[lson[u]] + 1;//从长儿子转移
    }
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa || v == lson[u] ) continue;
        f[v] = now;
        now += dep[v];//预留空闲位置
        dfs2( v, u );
        for( int i = 1; i <= dep[v]; i ++ ){//直接暴力合并
            f[u][i] += f[v][i - 1];//通过之前预留的位置直接合并
            if( f[u][i] > f[u][ans[u]] || ( f[u][i] == f[u][ans[u]] && i < ans[u] ) )
                ans[u] = i;
        }
    }
    if( f[u][ans[u]] == 1 ) ans[u] = 0;//子树是一条链
}
int main(){
    cin >> N;
    for( int i = 1; i < N; i ++ ){
        int x, y;
        cin >> x >> y;
        add( x, y ), add( y, x );
    }
    dfs( 1, 0 );
    f[1] = now, now += dep[1];
    dfs2( 1, 0 );
    for( int i = 1; i <= N; i ++ ){
        cout << ans[i] << "\n";
    }
    return 0;
}

:::

:::success[\(\textup{例2.?.2.2}\)]

这题倍增可以轻松卡过,但是我们并不能局限于这么低迷的复杂度。

预处理优化不了,而且瓶颈也是在查询,考虑面向标签?

就是将树剖开之后,好处就是我们向上跳的时候方便了,当然,要倍增预处理。

然后每次查询向上跳大约k步到它的链的链顶,再通过当前点向下的和向上的点(只有链顶有预处理)来找下面即可。

思路很简单,做起来四个小时算简单吗?

int f[MAXN][30], dep[MAXN], dis[MAXN];//dis[u]表示其子树的高度
int lson[MAXN], tp[MAXN];//链顶
vector<int> down[MAXN], up[MAXN];//存储链顶向上向下的前log个点
void dfs( int u, int fa ){
    f[u][0] = fa;
    dep[u] = dep[fa] + 1;
    dis[u] = 1;
    for( int i = 1; i <= 20; i ++ ){
        f[u][i] = f[f[u][i - 1]][i - 1];
    }
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u );
        if( dis[v] + 1 > dis[u] ){
            dis[u] = dis[v] + 1;
            lson[u] = v;
        }
    }
}

long long lstans = 0, ans = 0;

void dfs2( int u, int ltp ){
    down[ltp].push_back( u );
    tp[u] = ltp;
    if( lson[u] ) dfs2( lson[u], ltp );//处理重儿子
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == lson[u] ) continue;
        up[v].reserve(dis[v] + 1);
        up[v].push_back( v );
        for( int j = 1; j <= dis[v]; j ++ )
            up[v].push_back( f[up[v].back()][0] );//预处理
        dfs2( v, v );//预处理儿子,新链顶是自己
    }
}

int main(){
    IOS;
    N = read(), Q = read(), s = read();
    for( int i = 1; i <= N; i ++ ){
        int k;
        k = read();
        if( !k ) root = i;
        else add( k, i );
    }
    dfs( root, 0 );
    up[root].push_back( root );
    dfs2( root, root );
    for( int i = 1; i <= Q; i ++ ){
        long long x = ( get( s ) ^ lstans ) % N + 1;
        long long k = ( get( s ) ^ lstans ) % dep[x];
        if( !k ) lstans = x;
        else{
            int to = dep[x] - k;//目标祖先的深度
            x = tp[f[x][(int)(floor( log2(k) ))]];//跳出第一大步
            if( dep[x] <= to ){//如果当前深度还比目标深度要浅
                x = down[x][to - dep[x]];
            } else{//否则往下跳
                x = up[x][dep[x] - to];
            }
            lstans = x;
        }
        ans ^= ( lstans * i );
    }
    cout << ans;
    return 0;
}

:::


2.?+1 Dsu On Tree

中文名:树上启发式合并。

暴力要么时间过于高昂,要么空间过于抽象。

有没有能够平均一下的方法?有的。

我们考虑用多跑(时间)来换取记录下来的空间,反之,同理。

好吧正经一点说:

  • 把它按重链剖了;
  • 先跑轻儿子,解决其中的询问;
  • 出来,把重儿子跑了,然后把数据丢给这一轮的父亲,更新数据
  • 最后暴力跑一边轻儿子,把他们的数据跑出来给父亲。

和前面长链剖分优化dp的思路类似,就是独立处理子树然后选择最大的直接继承,这样就不用再跑一次重子树了。

那么时间复杂度?

重链不可能超过 \(\log\) 条,于是为 \(O( N \cdot \log{N} )\)

:::success[\(\textup{例2.?+1.1}\)]

几乎没什么改动的模板题。

把询问离线下来,按照上面所说的在子树里面跑,跑出来该删的删,该继承的继承。

最后再按原序回答即可,看代码清楚一点。

const int MAXN = 500005;
int N, M;
struct qwq{
    int id, h;
};

vector<qwq> q[MAXN];

struct star{
    int nxt, to;
}edge[MAXN << 1];
int head[MAXN], cnt;
void add( int u, int v ){
    edge[++ cnt] = { head[u], v };
    head[u] = cnt;
}

int tim = 0;
char col[MAXN];
int dep[MAXN], hson[MAXN];
int in[MAXN], out[MAXN], id[MAXN];//进入、离开的时间戳以及id[u]表示时间u对应的节点编号
int siz[MAXN], ccnt[MAXN][30];//cnt[d][c]表示深度为d的节点中颜色c出现的次数
bool ans[MAXN];//第i个查询的答案

void dfs( int u, int fa ){
    dep[u] = dep[fa] + 1;
    siz[u] = 1;
    in[u] = ++ tim;
    id[tim] = u;
    int nowmax = 0;
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == fa ) continue;
        dfs( v, u );
        siz[u] += siz[v];
        if( siz[v] > nowmax ) nowmax = siz[v], hson[u] = v;
    }
    out[u] = tim;
}

//在对应深度加入计数器
void addnd( int u ){ ccnt[dep[u]][col[u] - 'a' + 1] ++; }
//删除关于该节点的统计
void dltnd( int u ){ ccnt[dep[u]][col[u] - 'a' + 1] = 0; }

void addu( int u ){//将u对应的整颗子树加入统计(因为dfn序在同一子树应当是连续的)
    for( int i = in[u]; i <= out[u]; i ++ ){
        addnd( id[i] );
    }
}

void dltu( int u ){//删除同理
    for( int i = in[u]; i <= out[u]; i ++ ){
        dltnd( id[i] );
    }
}

bool chk( int d ){//查询深度d(当前子树)是否满足
    int tot = 0;
    for( int i = 1; i <= 26; i ++ ){
        if( ccnt[d][i] & 1 ){
            tot ++;
            if( tot > 1 ) return false;
        } 
    }
    return true;
}

void dotsolve( int u, bool kep ){
    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == hson[u] ) continue;//先处理轻儿子并且不保留信息
        dotsolve( v, false );
    }

    if( hson[u] ){//处理重儿子(保留信息)
        dotsolve( hson[u], true );
    }

    for( int i = head[u]; i; i = edge[i].nxt ){
        int v = edge[i].to;
        if( v == hson[u] ) continue;//将轻儿子信息暴力合并进u
        addu( v );
    }

    addnd( u );//计入当前节点

    for( qwq qqq : q[u] ){
        ans[qqq.id] = chk( qqq.h );
        //由于前面删除加入的特质,此处不会计入其他子树的节点
    }

    if( ! kep ) dltu( u );//如果是轻子节点就删除数据
}

int main(){
    ios::sync_with_stdio( 0 );
    cin.tie( 0 );
    cout.tie( 0 );
    cin >> N >> M;
    for( int i = 2; i <= N; i ++ ){
        int p;
        cin >> p;
        add( p, i );
    }
    for( int i = 1; i <= N; i ++ ) 
        cin >> col[i];
    for( int i = 1; i <= M; i ++ ){
        int u, d;
        cin >> u >> d;
        q[u].push_back({ i, d });
    }

    dfs( 1, 0 );
    dotsolve( 1, 0 );
    for( int i = 1; i <= M; i ++ ){
        cout << ( ans[i] ? "Yes\n" : "No\n" );
    }
    return 0;
}

:::


3.图论

3.? 网络流相关

概念:

  • 网络可以理解为是在一张有向图中,每条边的边权一般为其流量最大值。

  • 增广路表示一条从源点到汇点剩余流量大于 \(0\) 的路径。

  • 残量网络表示每条边都有残余流量的网络。

3.?.1 最大流

常见问题:给定源汇点,最大化整个网络的流量。

引入一个假的贪心,当有增广路的时候就走,更新。

我们将其修复正确,即为 FF 算法。

对于每一条边,我们对其建反边。

当有流量经过原先边时,我们更新其反边权值为当前所消耗的流量。

于是,实现了反贪,我们可以通过走反边来撤销流量。

EK 算法,是其的一个实现(优化),该算法的核心是:在残量网络中用 bfs 找一个条源点到汇点的增广路,其余都与前面相同。

看起来和我们原先的贪心很相似,但实际上我们会发现,这个算法使用的是 bfs,在时间和策略上都有保障。

时间复杂度:\(O(N \cdot M ^ 2)\)

啥?你说 EK 容易被卡太慢了?有没有更快的?

有的。

Dinic 算法可以高效地找出最大流,原理是通过在每次找增广路之后按照顺序标记分层图,再跑出答案。

正确性不会证,但是因此有点似懂非懂?

弧优化即为记录每个点找到上次扫到的那条边,减少循环,并不会证明。

时间复杂度:\(O(N ^ 2 \cdot M)\)

:::success[\(\textup{例3.?.1.1}\)]

cnt 记得从 1 开始,这样异或出来才是反边。

EK。

int N, M, S, T, ans;
bool vis[MAXN];
int dis[MAXN], pre[MAXN], flag[2510][2510];
//dis[i]表示从源点到i的路径最多能推多少流  pre[i]记录到达i所用的边(在edge中的下标)
//flag[u][v]表示u -> v的边在edge中的下标
struct star{
    int nxt, to, w;//w -> 当前便的残量
}edge[MAXN];
int head[MAXN], cnt = 1;
void add( int u, int v, int w ){
    edge[++ cnt] = { head[u], v, w };
    head[u] = cnt;
    edge[++ cnt] = { head[v], u, 0 };
    head[v] = cnt;
}

int bfs(){
    for( int i = 1; i <= N; i ++ ) vis[i] = 0;
    queue<int> q;
    q.push( S );
    vis[S] = 1, dis[S] = inf;
    while( !q.empty() ){
        int u = q.front();
        q.pop();
        for( int i = head[u]; i; i = edge[i].nxt ){
            if( edge[i].w == 0 ) continue;
            int v = edge[i].to;
            if( vis[v] ) continue;
            vis[v] = true;
            dis[v] = min( dis[u], edge[i].w );
            pre[v] = i, q.push( v );
            if( v == T ) return 1;
        }
    }
    return 0;//没找到增广路径
}

void upd(){
    int u = T;//从汇点回溯
    int nowflow = dis[T];
    while( u != S ){
        edge[pre[u]].w -= nowflow, edge[pre[u] ^ 1].w += nowflow;
        u = edge[pre[u] ^ 1].to;//反向边 
    }
    ans += nowflow;
}

signed main(){
    cin >> N >> M >> S >> T;
    for( int i = 1; i <= M; i ++ ){
        int u, v, w;
        cin >> u >> v >> w;
        if( flag[u][v] == 0 ){
            add( u, v, w );
            flag[u][v] = cnt;
        } else edge[flag[u][v] - 1].w += w;//因为一次性建了两条边,于是要减一
    }
    while( bfs() )//可以找到增广路径
        upd();
    cout << ans;
    return 0;
}

Dinic。

int N, M, S, T;
int dep[MAXN], cur[MAXN];//标记分层图深度 cur[i]表示点i循环到了哪一条边
struct star{
    int nxt, to, w;
}edge[MAXN << 1];
int head[MAXN], cnt = 1;
void add( int u, int v, int w ){
    edge[++ cnt] = { head[u], v, w };
    head[u] = cnt;
    edge[++ cnt] = { head[v], u, 0 };
    head[v] = cnt;
}
int ans;
bool bfs(){
    queue<int> q;
    for( int i = 1; i <= N; i ++ ) dep[i] = 0;
    dep[S] = 1, q.push( S );
    while( !q.empty() ){
        int u = q.front();
        q.pop();
        for( int i = head[u]; i; i = edge[i].nxt ){
            int v = edge[i].to;
            if( edge[i].w && !dep[v] ){
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    if( dep[T] == 0 ) return false;
    return true;
}

int dfs( int u, int dis ){//u->当前节点 dis->当前流量
    if( u == T ) return dis;
    for( int & i = cur[u]; i; i = edge[i].nxt ){//引用->同时更新cur数组
        int v = edge[i].to;
        if( dep[v] == dep[u] + 1 && edge[i].w != 0 ){
            int now = dfs( v, min( dis, edge[i].w ) );
            if( now ){
                edge[i].w -= now, edge[i ^ 1].w += now;
                return now;
            }
        }
    }
    return 0;
}

void dinic(){
    while( bfs() ){
        for( int i = 1; i <= N; i ++ ) cur[i] = head[i];//记得每次更新的到第一条边
        while( int now = dfs( S, INF ) ){
            ans += now;
        }
    }
}

signed main(){
    cin >> N >> M >> S >> T;
    for( int i = 1; i <= M; i ++ ){
        int u, v, w;
        cin >> u >> v >> w;
        add( u, v, w );
    }
    dinic();
    cout << ans;
    return 0;
}
posted @ 2026-02-05 08:29  Fαll  阅读(4)  评论(0)    收藏  举报