今天是杨溢鑫老师的讲授~
T1
物理题,不多说(其实是我物理不好qwq),注意考虑所有的情况,再就是公式要推对!
#include<bits/stdc++.h> using namespace std; typedef long long LL; const LL mod = 998244353; inline void rd(LL &x) { x=0;int f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} } LL n,m,x[2],y[2],g,ans; LL qpow(LL a) { LL res=1,k=mod-2; while(k) { if(k&1)res=res*a%mod; a=a*a%mod; k>>=1; } return res; } LL _2 = qpow(2); void down(LL x,LL h){x%=mod;h%=mod; ans += (x*x*m%mod)*qpow(4*h); ans%=mod;} void up(LL x,LL h) {x%=mod;h%=mod; ans += (m*h%mod) + ( (x*x*m%mod) *qpow(4*h) )%mod; ans%=mod;} void pi_4(LL x){x%=mod; ans += (m*x%mod)*_2; ans%=mod;} void solve() { scanf("%lld%lld%lld",&n,&m,&g); rd(x[1]); rd(y[1]); for(int i=2;i<=n;i++) { rd(x[i&1]); rd(y[i&1]); if(y[i&1]>y[!(i&1)]) up(x[i&1]-x[!(i&1)],y[i&1]-y[!(i&1)]); if(y[i&1]<y[!(i&1)]) down(x[i&1]-x[!(i&1)],y[!(i&1)]-y[i&1]); if(y[i&1]==y[!(i&1)]) pi_4(x[i&1]-x[!(i&1)]); } printf("%lldJ",ans*g%mod); } int main() { freopen("jump.in","r",stdin); freopen("jump.out","w",stdout); solve(); fclose(stdin); fclose(stdout); return 0; }
T2
#include <bits/stdc++.h> using std::max; using std::strlen; const int N=20010,Mlen=510,Node=N*Mlen*2; int n,len[N],ans; char s[N][Mlen]; struct Trie{ int son[Node][2],node,app[Node],dep[Node],f[Node]; void init() { son[0][0]=son[0][1]=0; app[0]=dep[0]=0; } int newnode(int d){ ++node; son[node][0]=son[node][1]=0; dep[node]=d; app[node]=0; return node; } void insert(char *s,int len,int t) { int p=0,add=0; while (t) { for (int i=0;i<len && add<=1000;++i) { int &x=son[p][s[i]-'0'],pp=p; p=x?x:x=newnode(dep[p]+1); f[p]=pp; app[p]++; add++; } t--; } } int FindAns() { int ans=0; for (int i=1;i<=node;++i) if (app[i]>=2) { ans=max(ans,dep[i]); } // for (int i=1;i<=node;++i) if (app[i]>=2 && ans==dep[i]) { // int p=i; // while (p) { // putchar((p==son[f[p]][1])+'0'); // p=f[p]; // } // putchar(' '); // break; // } return ans; } }tr; void Init() { scanf("%d",&n); for (int i=1;i<=n;++i) { scanf("%s",s[i]); len[i]=strlen(s[i]); } } void Solve() { tr.init(); for (int i=1;i<=n;++i) { tr.insert(s[i],len[i],(1000-1)/len[i]+1); } printf("%d ",tr.FindAns()); } int main() { freopen("bomb.in","r",stdin); freopen("bomb.out","w",stdout); Init(); Solve(); fclose(stdin); fclose(stdout); return 0; }
T3
正解
离散化成绩+权值线段树维护每个人的位置。
每次移动将人的位置移动到 n+k ( k 为操作次数 )。
注意一个细节,询问是寻找 x 前面权值最小的元素,那么需要获取实际情况中 pre_x 的权值。使用链表模拟即可。 最后枚举最终队列的队首,判断长度为n范围外有多少人需要调整。( 其实什么方法都可以搞搞 )
部分分
对于 10% 的数据:模拟;
对于 30% 的数据:n2 暴力模拟每个时刻的情况。
#include<bits/stdc++.h> using namespace std; typedef long long LL; #define mid ((l+r)>>1) const int N=2e5+10,inf=0x3f3f3f3f; int n,m,rt; int L[N<<1], R[N<<1], v[N<<1]; int a[N], b[N], pos[N], nxt[N], pre[N], cs, cnt; void build(int &i,int l,int r) { i = ++cs; if(l<r) build(L[i],l,mid), build(R[i],mid+1,r), v[i] = min( v[L[i]] , v[R[i]] ); else v[i] = pos[l]; } void alter(int i,int l,int r,int p) { if(l<r) { if(p<=mid) alter(L[i],l,mid,p); else alter(R[i],mid+1,r,p); v[i] = min(v[L[i]],v[R[i]]); } else v[i] = ++cnt; } inline int query(int i,int l,int r,int k) { while(l<r) { if(v[L[i]]<=k) i = L[i], r = mid; else i = R[i], l = mid + 1; } return l; } void solve() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]), b[i] = a[i]; sort(b+1,b+1+n); for(int i=1;i<=n;i++)//离散化a初始排列 { a[i] = lower_bound(b+1,b+1+n,a[i]) - b; nxt[a[i-1]] = a[i]; pre[a[i]] = a[i-1]; pos[a[i]] = i; } build(rt,1,n); cnt = n; int last = a[n];//last队尾 while(m--) { static char s[8]; static int x,t; scanf("%s%d",s,&x); t = lower_bound(b+1,b+1+n,x) - b; if(s[0]=='A') { if(!pre[t]) printf("-1 "); else printf("%d ",b[ query(rt,1,n,pos[pre[t]]) ]);//查询小于等于pos[pre_t]的最小值的人 } else { if(last==t)continue; alter(rt,1,n,t);//单点修改第t人pos pos[t] = cnt; nxt[pre[t]] = nxt[t]; pre[nxt[t]] = pre[t];//当前的前后驱修改 pre[t] = last; nxt[last] = t; last = t; } } sort(pos+1,pos+1+n); int tail = 1, ans = inf; for(int i=1;i<=n && tail<n;i++) { while(pos[tail] - pos[i] + 1<n && tail<=n) tail++; if(tail>n || pos[tail] - pos[i] + 1>n) tail--; ans = min(ans,i-1 + n-tail);//左右区间剩下的 } printf("%d ",ans); } int main() { freopen("queue.in","r",stdin); freopen("queue.out","w",stdout); solve(); fclose(stdin); fclose(stdout); return 0; }
基础数据结构及其扩展
大纲
树状数组:树状数组的基本操作及应用,多维树状数组;
一些常用思想: 树上倍增,,启发式合并, 树上差分,DFS 序的应用;
线段树: 线段树的基本操作及应用,线段树的合并,线段树的可持久化;
并查集:并查集的拓展以及带权并查集的应用;
树状数组
均摊修改和查询的复杂度,每个点保存部分和信息的数组;
特点: 实现简单,容易和其它数据结构嵌套;
常数因子小,复杂度容易分析;
扩展困难,增加功能的思维难度大;
树状数组的基本功能:
单点修改;区间查询;
单次操作的复杂度都为 O (log n),且常数很小;
树状数组维护的信息的特点:
如果我们每次只需要查一个前缀的信息,那么只要维护的信息满足可加性即可;
如果我们要查区间信息,那么还需要该信息存在逆运算 ( 并且该逆运算比较容易实现 );
树状数组功能的拓展:
多维树状数组
我们先想 ( 猜测 ) 一下二维该怎么做?
只需要加一层相似的循环结果;
多维同理 。
一些常用思想
树上倍增
预处理出从一个点开始向上 2i 个点的信息;
可以处理树上路径问题,然而空间复杂度比较大,常数也比较大;
实现起来和理解起来较简单;
启发式合并
如果在一道题目中,我们有若干个对集合的合并操作,我们可以将小的集合的元素依次插入大的集合之中,这样可以证明总插入次数是 ( n log n ) 的,实际上常数很小 。
树上差分
对于树上一条 u 到 v 的路径
我们可以将它差分为:
+u 到根的路径;
+v 到根的路径;
-lca ( u , v ) 到根的路径;
-lca ( u , v ) 的父亲到根的路径;
差分的用途
对一些东西进行直接统计时往往很复杂;
如果它满足可减性,可通过差分来解决问题或者降低复杂度;
DFS序
DFS 序: 每个节点在 DFS 中的进出栈的时间序列 ;
用途:将树的结构线性化;
例如下图的 DFS 序为:
DFS 序性质
任意子树在 DFS 序中都是连续的;
例如: 子树 b,g,h 对应的 dfs 序 bgghhb ;
两个节点之间的路径在 DFS 序中几乎连续, 对于不是祖先关系的两点 u,v, 取出他们在 dfs 序中对应的连续子序列,可以发现其中不在路径 上的点一定出现了两次,而路径上的点出了 LCA 都刚好出现一次, LCA 不出现;
例子: g,f 之间的路径在 dfs 序中为 ghhbceef
单点修改 + 子树查询
利用 dfs 序,可以轻松转为单点修改 + 区间查询;
树链修改 + 单点查询
利用树上差分,直接拆成四条从点到根的链;
由于是单点修改,所以考虑贡献和, 就转化为了: 单点修改 + 子树查询;
树链修改 + 子树和查询
树链修改处理方式同上一题;
我们假设 u 的子树中一节点 v 到根节点路径的总修改量为 w [ v ],则 v 对 u 的子树和贡献为:
w [ v ] * ( dep [ v ] - dep [ u ] + 1 ) = w [ v ] * ( dep [ v ] + 1 ) - w [ v ] * dep [ u ];
故以 u 为根的子树的总贡献为:
Σ w [ v ] * ( dep [ v ] + 1 ) - dep [ u ] * Σ w [ v ] ;
所以只需要用两个数据结构,一个维护 w [ v ] 的单点修改与区间求和, 另一个维护 w [ v ] * ( dep [ v ] + 1 ) 的单点修改与区间求和即可;
单点修改 + 树链和查询
树链修改处理方式同上一题;
考虑一个点 u 修改后对 v 到根节点的权值有影响当且仅当 u 为 v 的祖 先,所以每次修改相当于子树修改, 单点查询, 再利用 dfs 变为区间修改,单点查询;
线段树
简介
元素间相对位置不变时,快速维护区间信息的数据结构;
特点:
结构唯一固定,方便维护;
扩展灵活,功能强大;
可以支持合并和可持久化;
线段树的基本操作
单点修改,区间查询;
区间修改,单点查询;
区间修改,区间查询;
线段树实现细节
其实懒标记的思想是可以推广到很多树形数据结构的;
对于修改操作,递归到子树前,要先下放标记,子树递归完后,要记得合并儿子的信息;
对于询问操作, 递归到子树前, 要记得下方标记,子树递归完后,要记得合并来自儿子子树的答案的信息 。
线段树的核心问题
标记下放需要信息与标记的合并,标记与标记的合并;
合并子树信息需要信息与信息的合并;
所以我们只要解决了这三个合并的问题,也就可以轻松使用线段树了;
只能实现空节点间的相互替换;常数太大;
线段树的合并
对于两个线段树,如果他们范围相同,那么他们的形状一定是相同的;
线段树的合并能替代一部分需要启发式合并才能解决的问题 n 个初始只有 1 个元素的线段树, 两个两个合并最终合并在一起, 总复 杂度是 O ( n log n );
但由于常数较大, 实际效果与启发式合并不相上下;
线段树的可持久化
很明显的,如果我们要在原线段树上进行一次单点修改,并将新的树保存下来,我们其实可以不用新建 2n 个空间,我们可以注意到,每修改一次单点,只对从根到那个叶节点路径上的点有影响,所以我们新建一棵树只需 log n 的空间开销,正如图中所示,我们把新的节点的 另一个儿子,指向老的节点的一个儿子即可 (由于是修改,线段树的 结构是完全一样的,只是这 log n 个节点维护的信息不一样罢了。
主席树
对原序列的每个前缀建一棵权值线段树,维护每个数出现次数,然后发现这样的数据结构是可减的;
特点:
以权值为关键字来维护信息;
动态开节点以节省内存;
并查集
并查集的拓展以及带权并查集的应用
并查集
一般用来进行连通块的维护;
其实还有更多的应用;
并查集的优化
并查集有两个优化:
1 路径压缩: 在对一个点进行 find 操作后,让他直接指向他的祖先;
2 按秩合并: 按秩合并的基本思想是使包含较少结点的树的根指向包含较多结点的树的根,这里的 “大小” 除了节点数量外,也可抽象为数的高度。
使用其中一种,复杂度是 O( n log n ) 的, 两种都用,复杂度是 O ( n a ( n ) ) 。 在这里,a ( n ) 是阿克曼(Ackermann)函数的反函数。
并查集的可持久化
实现可持久化并查集相当于实现一个可持久化数组,我们直接使用可持久化线段树就行了。
撤销并查集的合并
我们将两个并查集合并以后 (两种优化都用了), 如果要求支持撤销操作 ( 每次撤销最后一次连边操作 ),应该怎么做?
直接撤销行不行?为什么? 可持久化线段树 ?
我们不对并查集进行路径压缩即可 。
带权并查集
我们可以对并查集中的元素维护他到并查集的根的信息,只要这个信息满足可加性即可。
当然如果这个信息的运算满足它是它自己的逆运算的话,我们还可以求出这个连通块任意两点之间的信息;
数据结构串讲
如果 X、Y 是同类,可以用并查集并起来;
但如果对于 A、B、C 三类分开保存,会存在一定问题;
因为 A、B、C 之间存在着连续关系,比如 A 吃 B、B 吃 C、C 吃 D,其实可以推导出 A、D 为同类;
考虑将关系建成一张图,两个点之间路径的权值由关系决定。
如果 u、v 是同类,那么 u->v 路径权值为 0;
如果 u 吃 v,那么 u->v 路径权值为 1;
如果 v 吃 u,那么 u->v 路径权值为 2;
那么两个点之间的关系可以由两个点之间的路径得出(模3)。
但每次查询时找路径会有一个 O ( N ) 的复杂度;
发现两个点之间的所有路径权值都是一样的,所以只需要保留一条;
两个点之间的关系由这个权值来表示;
知道了两个点分别与根的关系,就可以知道两个点之间的关系;
带权并查集压缩路径的时候需要考虑当前点到新根的距离;
复杂度 O ( K * alpha ( N ) );
我相信大家都会
如果把从初始状态变为末状态的每一个操作 ( a , b ) 看成是把原图 G1 中的 ( a , b ) 边拆掉,假设拆掉该边后将边( a , b ) 所在的连通块分成的两个集合为 X、Y 。那么我们需要在新图 G2 中把一个 X 集合中的点 a' 和一个 Y 集合中的点 b' 连起来。
如果拆掉 G1 中的边 ( a , b ) ,并且保证拆掉边后不会影响其他边的拆除(因为拆除一条边 ( a , b ) 需要保证 a , b 连通)。那么对于 X 、Y 来说,两个集合之间在 G1 中有且仅有 ( a , b ) 一条边。
如果要把 a' 和 b' 连起来,必须保证在集合 X 与集合 Y 在 G2 中不连通,也就是集合 X、Y 在 G2 中有且仅有 ( a' , b' ) 一条边相连 。
可以枚举拆哪条边,然后 O ( N ) 判断,复杂度 O ( N2 ),总复杂度为 O ( N3 );
可以知道这个算法的复杂度是无法被优化的。
既然正向思考不行,那么考虑反向思考;
在刚才是算法中,不管拆哪一条蓝边,得到的 X、Y 集合都是不同的;
但是在拆了边以后,两个端点不论在红图和蓝图中都是不连通的;
如果反向思考,考虑有了 X、Y 集合以后,将两个集合合并的条件是两个集合之间既有蓝边也有红边;
如果暴力求复杂度是 O ( N2 ) 的;
但是我们可以知道,在加入一条边后,新的可以合并的集合之一肯定是新的集合;
假设要合并 X、Y 两个集合,合并后产生新的集合 Z 。那么新产生的可以合并的两个集合中肯定有一个是集合 Z ,另一个集合肯定是与集合 X、Y 分别有两条不同颜色的边相连;
所以可以在合并集合时维护新的可以合并的集合,注意修改与新集合相连的红边和蓝边;
启发式合并
因为是与 X、Y 集合有两条不同颜色的边相连,因此只需要遍历小一点的集合;
这样会使复杂度更优,为什么?
很显然贪心可以看出;
对于一个大小为 A 的集合,将其与大小为 B 的集合合并(A <= B);
合并的复杂度为 O ( A ) ,合并后的集合大小为 O ( A+B );
相当于每次合并后集合大小会乘 2;
最终集合大小为N,因此合并次数不超过 log 次,复杂度为 O ( N * log ( N ) );
合并部分代码:
主函数代码:
KMP
一道 kmp 裸题,复习一下 kmp 算法(复习啥啊,我本来就不会qwq):
kmp 的精髓为继承的思想:
考虑在一个位置失配以后的转移;
虽然这个位置失配了,但是前面的位置的匹配情况是已经知道的,因此直接继承;
最多往前跳 n 次,复杂度 O ( N ) ;
如果不考虑 | S‘ | * 2 <= i,那就只需要在计算 fail 指针的时候用 num 数组记录跳跃次数;
最简单的处理方法是记录了跳跃次数后,在查询答案时查找满足长度条件前缀;
但是对于每个 i 都这样处理会超时,因为最坏情况下会跳 N 次;
其实可以用倍增优化跳跃,这样跳跃次数就降低到了 log;
复杂度 O ( N * log N )(可能Ac)
倍增部分代码:
kmp 的精髓是继承;
那这道题可否继承?
显然可以!
对于 i 位置满足条件的 S’,i+1 位置满足条件的 S’’ 其实是在 S’ 的基础上多了一个字符;
其实和普通的 kmp 匹配没什么区别;
复杂度为 O ( N );
继承部分代码:
第一反应应该是贪心,枚举一个串 A,找与它相配质量最大的另一个串 B;
但是可能 B 与串 C 匹配更优,于是贪心错误;
考虑更优秀的贪心;
在 trie 树上贪心;
将所有串加入 trie 树中,在深度较深的地方匹配会更优。
由于只需要知道最后的总质量,所以直接取每个点的子树中最大的匹配即可;
复杂度 O ( 输入字符总数 );
定义 dp [ i ][ j ][ s ] 表示到 i 号点,长度为 j,当前被包含串为 s 的最大权值;
直接 AC 自动机上转移即可(虽然并不会);
第二维可滚动;
关于 AC 自动机。。。
AC 自动机:
其实就是把 kmp 的 fail 指针加在 trie 树上;
考虑原本已经匹配好了的一个前缀,我们在这个前缀后面加上几个字符,就可能不相同了;
所以我们需要再往前找;
AC自动机 + DP:
假设只考虑根到叶子的路径,并且对每个串 S 单独考虑;
可以得到个十分浅显的 dp;
dp [ i ][ j ] 为在 i 号节点、当前能否匹配到位置 j;
dp [ i ][ j ] = dp [ fa ][ j-1 ] & ( c [ i ] == S [ j ] );
需要枚举根,复杂度为 O ( N3 );
枚举根,然后求出根到叶子节点所形成的字符串,这是个 O ( n2 ) 的算法;
这仅仅是找到一个串,那么总的暴力复杂度就是:O ( m * n2 ),显然过不去;
考虑能不能把 0/1 换掉;
int 可以表示 32 个 0/1 ,long long 可以表示 64 个 0/1 ,所以我们可以用 bitset;
dp [ i ][ j ] -> dp [ son ][ j+1 ] , son ∈ i;
dp [ i ][ j ][ s ] & c == 串的第 j+1 个位置;
时间复杂度:O ( 26 * m );
s 表示 m 个串中的第 s 个串;
为什么枚举根呢?
因为有的字符串会跨越根:
那么我们可以看做从根分别往下找,最后再合并起来;
up [ i ][ j ]: 以 i 号节点结束(可以经过根),匹配长度为 j 的字符串是否可行;
down:[ i ][ j ] :从 i 号往下走,匹配长度为 j 的字符串是否可行;
每次合并 up 和 down,时间复杂度是 O ( n ) 的,所以总复杂度是 O ( n2 );
bitset
由于 dp 所存储的 down 和 up 都是 bool 类型的变量,因此可以用 bitset 存储;
对于所有 j,up 和 down 都是从 j-1 的位置转移过来的。
可以直接用 bitset 转移,复杂度 O ( N / 32 );
bitset 也可以直接合并;
总复杂度 O ( N2 / 32 );
假设确定了 i 号节点,那么就应该是在i号点的子树中从小到大选择,直到代价和大于 M;
可以使用堆来帮助选择,复杂度 O ( N2 * log ( N ) );
从下到上考虑,在 i 号节点所选的点一定是 在它的儿子节点中选的点或者 i 号节点;
所以只需要将它的儿子中选的点拿出来建堆。
同样可以考虑启发式合并,复杂度为 O ( N * log ( N )2 );
可并堆(左偏树)
其实这个是涉及到两个堆合并的问题,那么就可以使用可并堆了;
左偏树是一种可并堆;
考虑刚才的启发式合并,其实它将堆的顺序打乱了,也就是它没有利用堆中原本的大小顺序;
其实这个大小顺序是可以利用的;
首先可以比较两个堆的根的大小顺序;
考虑将另一个堆插入左儿子还是右儿子,其实不论插入哪边都是可以的;
那么插入哪一边复杂度更小?
合并与层数有关;
插入层数较小的一边复杂度更优;
这样也能保证层数不会增加太多;
每次合并复杂度为 O ( log ( N ) );
总复杂度为 O ( N * log ( N ) );
对于一段区间 [ l , r ], 假设在右侧加入一个元素 r+1;
对于 [ l , r+1 ] 中的最小值所在位置 x,代价为 ( x - l + 1 ) * a [ x ];
再递归求 [ x+1 , r+1 ] 的代价;
最坏复杂度为 O ( N );
但对于 [ x+1 , r+1 ],代价是确定的,可以直接预处理;
对于 r+1,找出 [ l , r ] 中第一个小于 a [ r+1 ] 的位置 y,直到找到的位置为 x;
可以发现 [ x+1 , r+1 ] 段的代价与 x 的值无关,只和位置有关;
定义 s [ i ] 为 [ 1 , i ] 的代价,可以用 s [ r+1 ] - s [ x ] 得到 [ x+1 , r+1 ] 的代价;
不存在 [ x+1 , r+1 ] 有一个比 a [ x ] 更小的值使得 s [ r+1 ] 的左端能取到更优的值;
一次加入元素复杂度为 O ( 1 );
删除元素也是类似,复杂度 O ( 1 );
那么现在就可以:
由 [ l , r ] 得到 [ l , r+1 ];
由 [ l , r ] 得到 [ l+1 , r ];
由 [ l , r ] 得到 [ l , r-1 ];
由 [ l , r ] 得到 [ l-1 , r ];
可以直接用莫队实现,复杂度 O ( N * sqrt ( N ) );
复杂度证明应该都会
如果是二维或者一维,那很明显就是树状数组了;
但是这个题是三维,还能用树状数组吗?
你真的理解了树状数组吗?
三维树状数组
对于二维的树状数组来说,区间处理利用了容斥;
所以三维也是可以通过容斥处理区间;
反正都是区间反转不论怎么做都是对的
由于一个村庄 i 被覆盖的条件是在距离第 i 个村庄不超过 Si 的范围内建立了一个通讯基站;
因此可以发现能覆盖一个村庄的基站位置应该是一个区间;
定义状态 dp [ i ][ j ] 表示前 i 个村庄放了 j 个基站、并且第 j 个基站在 i 位置的代价;
转移时枚举下一个基站的位置 k,并计算区间 [ i , k ] 之间的村庄的代价;
复杂度为 O ( N2 * K );
但是这个转移没有利用到性质;
能覆盖一个村庄的基站位置应该是一个区间;
可否考虑到哪个地方为止某个村庄永远不会被以后的基站覆盖?
考虑第i个村庄对于的区间 [ L , R ];
如果目前考虑的最后一个基站为 R;
如果 R 处不建基站:
那么对于最后一个基站为 [ 1 , L-1 ] 的情况,都无法覆盖当前村庄;
因此需要对 [ 1 , L-1 ] 区间加 w [ R ];
如果 R 处建基站,那么相当于是在上一个基站为 [ 1 , R-1 ] 中选择一个代价最小的,相当于区间查询;
因此可以用线段树优化转移;
注意需要预处理最后一个基站为 j 时,j 以前的所有村庄的代价,并把这个值作为初始值加入计算;
因为最后一个基站的位置不确定,因此可以建立一个虚拟节点统计答案;
复杂度 O ( N * K * log ( N ) );
这是一个超级裸的题
左儿子和右儿子内部的逆序对是不会相互影响的;
将两部分单独处理再考虑合并;
合并的时候需要判断左儿子在前还是右儿子在前;
枚举节点较少的部分,二分查询另一部分中权值更大的数目【类似启发式合并】;
利用权值线段树计算;
合并线段树的时候,∑ [ l , mid ] * [ mid+1 , r ] 即为需要计算的答案;
复杂度 O ( N * log ( N ) );
对于两棵线段树;
当前节点为空时直接合并;
否则分别合并两个儿子;
复杂度为两棵数重合节点个数;
单独求一遍 Manacher;
对 A、B 倒序求一遍后缀数组;
枚举回文中心,再判断部分回文位置;
后缀数组;
二分找 i、j;
对两个串求最长公共前缀和最长公共后缀;
虚树
将图中的一些节点提出,保留树的形态不变;
用这些取出的关键点代表这棵树;
取出关键点建立虚树;
对虚树上的部分点进行 dp 找距离每个点最近的关键点;
考虑两个部分点之间的路径上的点应该如何选择;
答案与该路径上的分岔点无关;
复杂度 O ( 总关键点个数 );