【搬】板子整理计划
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[简化题意]
- \(p\) 中的区间[x, y]建新树
- \(t\) 中的树放到 \(p\) 里并删除
- \(p\) 中加入 \(x\) 个给出数字
- 查询 \(p\) 中[x, y]范围内的树
- 查询其中第 \(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\) 回收不要的点编号,用的时候丢进去即可。
:::
- 此处分裂代码
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;
}

浙公网安备 33010602011771号