目录
T1 异或 / 性质 / 分治
题意:
给你一个数 n n n ,让你求 ∑ i = 1 n − 1 i ⨁ ( n − i ) \sum_{i=1}^{n-1}i\ \bigoplus \ (n-i) i=1∑n−1i ⨁ (n−i)
其中 n ≤ 1 0 500 n \leq 10^{500} n≤10500
看到异或,首先想到按位考虑,简单打表,会发现一个性质:
若 n n n 为奇数,则 i ⨁ ( n − i ) i\bigoplus (n-i) i⨁(n−i) 在个位上对答案的贡献为 1
若 n n n 为偶数,则 i ⨁ ( n − i ) i\bigoplus (n-i) i⨁(n−i) 在个位上没有贡献
个位上的贡献是好求的
由此我们可以考虑将 n n n 从 二进制下 最低位开始,每次计算这一位的贡献,然后递归到 下一位,即 n > > 1 n>>1 n>>1 时的子问题
明确了方向我们来开始讨论,记 f ( n ) = ∑ i = 1 n − 1 i ⨁ ( n − i ) f(n) = \sum_{i=1}^{n-1}i\ \bigoplus \ (n-i) f(n)=∑i=1n−1i ⨁ (n−i)
- 若 n n n 为奇数,令 n = 2 k + 1 n = 2k+1 n=2k+1
f ( n ) = ∑ i = 1 2 k i ⨁ ( 2 k + 1 − i ) f(n)=\sum_{i=1}^{2k}i\ \bigoplus \ (2k+1-i) f(n)=∑i=12ki ⨁ (2k+1−i)
手写几项发现: 1 ⨁ 2 k , 2 ⨁ 2 k − 1 , . . . . , 2 k − 1 ⨁ 2 , 2 k ⨁ 1 1\bigoplus2k \ , \ 2\bigoplus2k-1 , .... , 2k-1\bigoplus2 , 2k\bigoplus1 1⨁2k , 2⨁2k−1,....,2k−1⨁2,2k⨁1 首尾相同,可以结合,即:
= 2 ∑ i = 1 k i ⨁ ( 2 k + 1 − i ) = 2\sum_{i=1}^{k}i\bigoplus (2k+1-i) =2∑i=1ki⨁(2k+1−i)
那么 i i i 与 2 k + 1 − i 2k+1-i 2k+1−i 必定一奇一偶,然而到底是哪个并不确定,那么更改求和的顺序
= 2 ∑ i = 1 k 2 i ⨁ ( 2 k + 1 − 2 i ) = 2\sum_{i=1}^{k}2i\bigoplus (2k+1-2i) =2∑i=1k2i⨁(2k+1−2i)
这么看就可以单拎出个位的贡献了
= 2 ∑ i = 1 k 2 i ⨁ ( 2 k − 2 i ) + 2 k = 2\sum_{i=1}^{k}2i\bigoplus (2k-2i) + 2k =2∑i=1k2i⨁(2k−2i)+2k
= 4 ∑ i = 1 k i ⨁ ( k − i ) + 2 k = 4\sum_{i=1}^{k}i\bigoplus (k-i) + 2k =4∑i=1ki⨁(k−i)+2k
此时就达到了使 n > > 1 n>>1 n>>1 的目标,那么与 f ( n ) f(n) f(n) 化成相同形式
= 4 ∑ i = 1 k − 1 i ⨁ ( k − i ) + 6 k = 4\sum_{i=1}^{k-1}i\bigoplus (k-i) + 6k =4∑i=1k−1i⨁(k−i)+6k
= 4 f ( k ) + 6 k = 4f(k) + 6k =4f(k)+6k
2.若 n n n 为偶数,令 n = 2 k n = 2k n=2k
f ( n ) = ∑ i = 1 2 k − 1 i ⨁ ( 2 k − i ) f(n)=\sum_{i=1}^{2k-1}i\ \bigoplus \ (2k-i) f(n)=∑i=12k−1i ⨁ (2k−i)
我们的目的是 直接清除掉每对数的个位 ,但这个操作对于奇偶数的修改是不同的,那么将奇偶分拎出来
= ∑ i = 1 k − 1 ( 2 i ) ⨁ ( 2 k − 2 i ) + ∑ i = 1 k ( 2 i − 1 ) ⨁ ( 2 k − 2 i + 1 ) \sum_{i=1}^{k-1}(2i)\ \bigoplus \ (2k-2i) \ + \ \sum_{i=1}^{k}(2i-1)\ \bigoplus \ (2k-2i+1) ∑i=1k−1(2i) ⨁ (2k−2i) + ∑i=1k(2i−1) ⨁ (2k−2i+1)
左侧式子同上
= 2 ∑ i = 1 k − 1 i ⨁ ( k − i ) 2\sum_{i=1}^{k-1}i\ \bigoplus \ (k-i) 2∑i=1k−1i ⨁ (k−i)
= 2 f ( k ) 2f(k) 2f(k)
右侧式子,个位的 1 对答案没有影响,直接减掉 1,化为偶数的情况
= ∑ i = 1 k ( 2 i − 2 ) ⨁ ( 2 k − 2 i ) \sum_{i=1}^{k}(2i-2)\ \bigoplus \ (2k-2i) ∑i=1k(2i−2) ⨁ (2k−2i)
= 2 ∑ i = 1 k ( i − 1 ) ⨁ ( k − i ) 2\sum_{i=1}^{k}(i-1)\ \bigoplus \ (k-i) 2∑i=1k(i−1) ⨁ (k−i)
化为同样的形式:
= 2 ∑ i = 0 k − 1 i ⨁ ( k − i − 1 ) 2\sum_{i=0}^{k-1}i\ \bigoplus \ (k-i-1) 2∑i=0k−1i ⨁ (k−i−1)
= 2 ∑ i = 1 k − 2 i ⨁ ( k − i − 1 ) + 2 ( k − 1 ) + 2 ( k − 1 ) 2\sum_{i=1}^{k-2}i\ \bigoplus \ (k-i-1)+2(k-1)+2(k-1) 2∑i=1k−2i ⨁ (k−i−1)+2(k−1)+2(k−1)
= 2 ∑ i = 1 k − 2 i ⨁ ( k − i − 1 ) + 4 k − 4 2\sum_{i=1}^{k-2}i\ \bigoplus \ (k-i-1)+4k-4 2∑i=1k−2i ⨁ (k−i−1)+4k−4
= 2 f ( k − 1 ) + 4 k − 4 2f(k-1)+4k-4 2f(k−1)+4k−4
所以 f ( n ) = 2 f ( k ) + 2 f ( k − 1 ) + 4 k − 4 f(n)=2f(k)+2f(k-1)+4k-4 f(n)=2f(k)+2f(k−1)+4k−4
复杂度分析:
n n n 为奇数时,搜索树只会单叉进入下一层;
n n n 为偶数时 n 2 \frac{n}{2} 2n 与 n 2 − 1 \frac{n}{2}-1 2n−1 相差不会太大,加上记忆化感性理解一下分出的两叉很快会汇合,同样状态不会太多
那么本题可过
T2 线段树原理 / DP / 记忆化
T3 区间DP思想 / 重要技巧
数据范围上看,大概率是
O
(
n
3
)
O(n^3)
O(n3) 的算法
简单打表发现:对于一段连续的 0 / 1 0/1 0/1,删除哪一个都一样。遇到这样的问题时,我们可以 钦定每个连续段只能从最后开始删,这样得到的方案 可以保证与 原来的合法方案 一一对应
由于数只能一个一个删,容易想到以长度为阶段做 区间DP
那么在枚举 l , r l,r l,r 的过程中,应当有 c h [ r ] ! = c h [ r + 1 ] ch[r]!=ch[r+1] ch[r]!=ch[r+1],否则这样的状态是无效的,转移时也不会用到
现在考虑如何合并区间的状态:
枚举一个断点 k k k , 合并时既要保证 1. [ l , k − 1 ] [l,k-1] [l,k−1] 与 [ k + 1 , r ] [k+1,r] [k+1,r] 的方案相互独立,严格互不影响;2. 不同的断点划分出的方案一定不同,且涵盖所有情况
这道题告诉我们:在区间 DP 中,限制 1 1 1 的重要性远远大于 限制 2 2 2 !子区间的合并才是核心,而限制 2 2 2 则是易于维护的
本题来说,我们为了使得两段独立,直接钦定 断点 k 是区间 [ l , r ] [l,r] [l,r] 中最后删除的 ,在其删除之前,左右严格互不影响,可以合并
再考虑,合并出来的方案可以是删一个左,删一个右,因此最终答案应该是 d p [ l , k − 1 ] ∗ d p [ k + 1 ] [ r ] ∗ C ( r − l , k − l ) dp[l,k-1]*dp[k+1][r]*C(r-l,k-l) dp[l,k−1]∗dp[k+1][r]∗C(r−l,k−l)
这样,最后删的点不同,确保了 方案不重 ;枚举所有可能被最后删除的点,确保了 不漏
我们做到了 不重不漏
此外,对断点 k k k 的限制:
1.是当前区间内每个连续段的开头
2.准备删去这个点时,当前区间其余点已删完,同时保证 这个点与后面的点组成的连续段中,它是所在连续段的最后一个(这样才能删)
结合我们第一步的转化,这两点都是比较自然的,但这些细节很重要
#include<bits/stdc++.h>
using namespace std ;
const int N = 450 ;
typedef long long LL ;
const LL mod = 1e9 + 7 ;
int n ;
char ch[N] ;
LL dp[N][N] , C[N][N] ;
/*
区间DP
考虑如何合并两区间,问题在于断点两侧的区间可能不独立,删去断点后会使得两个区间合并,影响方案
那么我们再次钦定:断点务必最后删。如此,两侧就独立了
最后删的点是有限制的:
1.是当前区间内每个连续段的开头
2.准备删去这个点时,当前区间已删完,要保证这个点必须成为 其所在连续段的最后一个
*/
void pre()
{
for(int i = 1 ; i <= n ; i ++ ) if( ch[i] != ch[i+1] || i == n ) dp[i][i] = 1 ;
C[0][0] = 1 ;
for(int i = 1 ; i <= n ; i ++ ) {
C[i][0] = 1 ;
for(int j = 1 ; j <= n ; j ++ ) {
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % mod ;
}
}
}
int main()
{
scanf("%d\n%s" , &n , ch+1 ) ;
pre() ;
for(int ln = 2 ; ln <= n ; ln ++ ) {
for(int l = 1 ; l+ln-1 <= n ; l ++ ) {
int r = l+ln-1 ;
if( r != n && ch[r] == ch[r+1] ) continue ; // 第一步转化
for(int k = l+1 ; k < r ; k ++ ) {
if( ch[k-1] != ch[k] && ( r == n || ch[k] != ch[r+1] ) ) { // 分别对应 1,2
dp[l][r] = ( dp[l][r] + dp[l][k-1]*dp[k+1][r]%mod * C[r-l][k-l]%mod ) ;
}
}
if( r==n || ch[l] != ch[r+1] ) dp[l][r] = ( dp[l][r] + dp[l+1][r] ) % mod ; // l=k特殊处理
dp[l][r] = ( dp[l][r] + dp[l][r-1] ) % mod ; // r=k特殊处理,断点的限制在上面已经判过
}
}
printf("%lld" , dp[1][n] ) ;
return 0 ;
}
T4 矩阵分治 / 计数技巧
T5 基础构造 / 打表题
T6 微扰排序 – 贪心中的邻项交换 / 线段树优化 DP
T7 异或 / 线性基
T8 神秘构造题
T9 线段树好题
T10 前缀和 [ 区间逆序对 ]
T11 DP好题 / 决策方式
T12 [星标] 计数技巧
第一眼看到的当然是
m
=
0
m=0
m=0 的
10
p
t
s
10pts
10pts 啦 ~
那么我们直接开始推式子:
1 2 ∑ i = 1 n ∑ j = 1 n ∑ k = 1 n ∑ p = 1 n ∣ i − k ∣ + ∣ j − p ∣ \frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{n}\sum_{k=1}^{n}\sum_{p=1}^{n}|i-k|+|j-p| 21∑i=1n∑j=1n∑k=1n∑p=1n∣i−k∣+∣j−p∣
= n 2 ∑ i = 1 n ∑ k = 1 n ∣ i − k ∣ =n^2\sum_{i=1}^{n}\sum_{k=1}^{n}|i-k| =n2∑i=1n∑k=1n∣i−k∣
= 2 n 2 ∑ i = 1 n ∑ k = i n ( k − i ) =2n^2\sum_{i=1}^{n}\sum_{k=i}^{n}(k-i) =2n2∑i=1n∑k=in(k−i)
= 2 n 2 ∑ i = 0 n − 1 i ( n − i ) =2n^2\sum_{i=0}^{n-1}i(n-i) =2n2∑i=0n−1i(n−i)
= 2 n 2 ∑ i = 1 n n i − i 2 =2n^2\sum_{i=1}^{n}ni-i^2 =2n2∑i=1nni−i2
= 2 n 2 ( n ∑ i = 1 n i − ∑ i = 1 n i 2 ) =2n^2(n\sum_{i=1}^{n}i \ - \ \sum_{i=1}^{n}i^2) =2n2(n∑i=1ni − ∑i=1ni2)
= 2 n 2 ( n 2 ( n + 1 ) 2 − n ( n + 1 ) ( 2 n + 1 ) 6 ) =2n^2(\frac{n^2(n+1)}{2}-\frac{n(n+1)(2n+1)}{6}) =2n2(2n2(n+1)−6n(n+1)(2n+1))
= n 3 ( n + 1 ) ( n − 1 ) 3 =\frac{n^3(n+1)(n-1)}{3} =3n3(n+1)(n−1)
这是不考虑加入边的答案
接下来:
第一步,我们钦定每个点对都是从上往下走,这样可以把加入边分成两类:斜率大于 0 和 小于 0 的
由于走新加入边,长度只比原来的曼哈顿距离少 1 1 1 ,也就是说 只有恰好经过这条边的路径才能使得答案减少,不可能回头路专门走新加边
那么二者独立,可以先计算斜率小于 0 的,再将大于 0 的左右对称计算
如图,上下两块绿色区域中的点 之间的路径 才能让答案减少
1
1
1
第二步
考虑枚举出发点(常用套路),那么当前所有的可用边已经确定
(1)显然,只有起点在当前枚举的点右下方的边才有用
(2)对于每条边来说,它能影响的范围只有 以其终点作为左上角的矩形,我们将这样的矩形并起来,会得到一个阶梯状的图形
(3)从上图也可以发现,部分区域内的点对当前出发点的贡献可能不止是 1 (右下角蓝色 标2矩形 )
这是一条路径中走过多条边的结果
如何快速得到这个权值呢?
我们可以预处理一个 d i s dis dis 数组,表示从 第 i i i 条边(终点)到 第 j j j 条边(起点)能经过的最多边数(包括 i , j ),类似 f l o y d floyd floyd 转移即可
对于目前这个 出发点 ,找一条对其贡献只能为 1 的边,用这条边作为
d
i
s
dis
dis 中的
i
i
i(不重要)
这样就得到了这个权值
(4)我们还会发现,权值较大的“阶梯”一定包含在权值小的“阶梯”内,那么总贡献就是所有权值阶梯的面积之和(不用再乘权值)
如何计算每个阶梯面积呢?—— 重要计数技巧:
将每条边的终点按从左往右排序,依次枚举,同时对于每个权值为 i i i 的阶梯,维护一个 h [ i ] h[i] h[i] 数组,表示其当前最大高度为多少
遇到一个新的终点 p p p 时,先考虑在不在已加入的边的集合内,若在,设其权值 (在(3)中提到的) 为 i i i
如果 x p < h [ i ] x_p < h[i] xp<h[i] ,如上图,则 x x x 会贡献一个新的矩形面积,这个矩形的长宽都是比较好算的
更新 a n s ans ans 即可
总结一下,我们目前完成了:
出发点一定,将合法的边加入集合内,计算所有阶梯面积和——即当前出发点对答案的总贡献
这样的操作
第三步
观察,如果我们从右下角开始枚举起点,一行一行考虑,那么从右往左顺次加边就好,每次加完后计算一下当前起点贡献
注意到 m m m 边数 的范围 远远小于 n n n 方格图边长
我们不难想到离散化
离散点的贡献怎么处理?继续找规律——
我们发现,在每一个红色矩形中,不管以哪个点作为起点,其贡献都是一样的,这个矩形面积也很好求
那么离散完后,只需枚举 边长为 m m m 量级的方格图 中每个点,计算贡献用 (当前ans) * (同贡献矩形面积即可)
这里需要再补充一点:
(2)中,用 dis 数组每次扫描来得到每个点的权值,但正解的复杂度不允许我们这样做
那么我们在处理一个 f [ i ] f[i] f[i] 数组,表示 第 i i i 条边 的终点的权值,从右往左扫的过程中用新加入边的 dis 更新所有 f [ i ] f[i] f[i]
#include<bits/stdc++.h>
using namespace std ;
#define int long long
const int N = 520 ;
typedef long long LL ;
const int mod = 998244353 ;
int n , m , tot , len , len2 , S ; // tot 为总边数
struct nn
{
int ax , ay , bx , by , id ;
}e[N] , eru[N] ;
int t ;
struct nod
{
int px , py , id ;
}ed[2*N] ;
int xt[2*N] , yt[2*N] ;
map<int,int> xpos , ypos ; // 原坐标映射到 m*m 中,坐标
LL xnow[2*N] , ynow[2*N] ; // m*m 中映射到 原坐标
void discrete() // 离散化
{
xpos.clear() ;
ypos.clear() ;
for(int i = 1 ; i <= len ; i ++ ) {
xt[2*i-1] = e[i].ax , xt[2*i] = e[i].bx ;
yt[2*i-1] = e[i].ay , yt[2*i] = e[i].by ;
}
sort( xt+1 , xt+2*len+1 ) ;
int cnt = 0 ;
for(int i = 1 ; i <= 2*len ; i ++ ) {
if( i == 1 || xt[i] != xt[i-1] ) {
xpos[xt[i]] = ++cnt ;
xnow[cnt] = xt[i] ;
}
}
n = cnt ;
sort( yt+1 , yt+2*len+1 ) ;
cnt = 0 ;
for(int i = 1 ; i <= 2*len ; i ++ ) {
if( i == 1 || yt[i] != yt[i-1] ) {
ypos[yt[i]] = ++cnt ;
ynow[cnt] = yt[i] ;
}
}
m = cnt ;
for(int i = 1 ; i <= len ; i ++ ) {
e[i].ax = xpos[e[i].ax] , e[i].bx = xpos[e[i].bx] ;
e[i].ay = ypos[e[i].ay] , e[i].by = ypos[e[i].by] ;
}
xnow[n+1] = S+1 ;
ynow[m+1] = S+1 ;
}
int dis[N][N] ; // i 号边(终点) 到 j号边(起点),最多经过的边数(包含i,j),通过 floyd 转移
void pre_work()
{
for(int i = 1 ; i <= len ; i ++ ) { // 先只用两条边转移
for(int j = 1 ; j <= len ; j ++ ) {
if( i == j ) {
dis[i][j] = 1 ;
}
else if( e[i].bx <= e[j].ax && e[i].by <= e[j].ay ) {
dis[i][j] = 2 ;
}
}
}
for(int k = 1 ; k <= len ; k ++ ) {
for(int i = 1 ; i <= len ; i ++ ) {
for(int j = 1 ; j <= len ; j ++ ) {
if( i == j || i == k || j == k ) continue ; // 枚举3条互不相同的转移
if( e[i].by <= e[k].ay && e[k].by <= e[j].ay ) { // 行
if( e[i].bx <= e[k].ax && e[k].bx <= e[j].ax ) { // 列
dis[i][j] = max( dis[i][j] , dis[i][k] + dis[k][j] ) ;
}
}
}
}
}
}
bool cmp1( nn x , nn y )
{
return x.ay < y.ay ; // 按起点横坐标排序,维护加边顺序
}
bool cmp2( nod x , nod y )
{
return x.py < y.py ;
}
LL ans ;
int f[N] , h[N] ;
void solve()
{
if( !len ) return ;
discrete() ;
// 此时, len 为总边数 , e 为边集 ,n 为行数 , m 为列数
sort( e+1 , e+len+1 , cmp1 ) ;
for(int i = 1 ; i <= len ; i ++ ) { // e 数组只代表起点, ed数组代表终点,二者分开考虑,同时保留编号,后面有用
e[i].id = ed[i].id = i ;
ed[i].px = e[i].bx , ed[i].py = e[i].by ;
}
sort( ed+1 , ed+len+1 , cmp2 ) ;
pre_work() ;
// 处理 dis 数组 , 保证 dis[i][j] 对应 e 中下标 (i,j)
int p ;
for(int i = n ; i >= 1 ; i -- ) {
memset( f , 0 , sizeof f ) ; // f[i] 表示 i 号边的终点,对应的 贡献值 是多少
p = len ; // 当前行加到哪条边 , len时横坐标最大
for(int j = m ; j >= 1 ; j -- ) {
// 考虑以 (i,j) 为起点
// 1. 尝试加边
while( p && e[p].ay == j ) {
if( e[p].ax >= i ) { // 有效边
f[e[p].id] = 1 ; // 自己贡献一个
for(int k = p+1 ; k <= len ; k ++ ) { // 用这条有效边去更新 已经添加进待选集合的所有边 的状态
if( e[k].ax < i ) continue ;
if( e[p].by <= e[k].ay && e[p].bx <= e[k].ax ) {
f[e[k].id] = max( f[e[k].id] , dis[p][k] ) ;
}
}
}
p -- ;
}
// 2. 通过 f 数组计算当前起点贡献
for(int i = 1 ; i <= len ; i ++ ) h[i] = n+1 ; // 初始时,每个终点都在最下面
LL res = 0 ;
for(int k = 1 ; k <= len ; k ++ ) {
int nam = f[ed[k].id] ;
if( !nam || ed[k].px < i ) continue ;
if( ed[k].px >= h[nam] ) continue ; // 无贡献
res += 1LL * (xnow[h[nam]]-xnow[ed[k].px]) * (S-ynow[ed[k].py]+1) % mod ; // 矩形面积
res %= mod ;
h[nam] = ed[k].px ;
}
ans = ( ans - res*(ynow[j]-ynow[j-1])%mod*(xnow[i]-xnow[i-1])%mod + mod ) % mod ;
}
}
}
signed main()
{
scanf("%lld%lld" , &tot , &S ) ;
int ax , ay , bx , by ;
for(int i = 1 ; i <= tot ; i ++ ) {
scanf("%lld%lld%lld%lld" , &ax , &ay , &bx , &by ) ;
if( ax > bx ) swap( ax , bx ) , swap( ay , by ) ;
if( ay < by ) {
e[++len] = (nn){ ax , ay , bx , by , 0 } ; // 左上到右下
}
else {
eru[++len2] = (nn){ ax , S+1-ay , bx , S+1-by , 0 } ; // 右上到左下,左右对称一下
}
}
ans = 2LL*S%mod*S%mod*S%mod*(S+1)%mod*(S-1)%mod*166374059%mod ;
solve() ;
for(int i = 1 ; i <= len2 ; i ++ ) {
e[i] = eru[i] ;
}
len = len2 ;
solve() ;
printf("%lld" , ans ) ;
return 0 ;
}
T13 树上操作 —— 动态维护直径
对于操作 1,删边过程不易维护,我们考虑倒着加边,能够使得在任何一个操作后,每个连通块内都符合整棵树的结构,那么就可以预处理整棵树的信息,用在连通块上。由加边容易想到并查集维护
对于 操作 2,我们可以联想到离点 u u u 最远,显然是直径端点。那么这道题的问题就变得清晰:动态维护每个连通块的直径
我们来回忆直径有哪些性质:
1.两端点必定是叶子(无根)
2.从任意一点出发走最远都能到达直径端点,重点在证明这条性质用到的 反证法 上,貌似很多问题都可以往这个反证上想
还有就是一定要打表!!!
总之,基于直觉、打表和反证,我们会发现 合并后整块的直径端点 必定在原来两个子图的直径上的四个端点之中(从反证角度理解)
那么维护就变得简单了
#include<bits/stdc++.h>
using namespace std ;
const int N = 2e5 + 10 ;
typedef long long LL ;
int read()
{
int x = 0 ; char c = getchar() ;
while( c < '0' || c > '9' ) c = getchar() ;
while( c >= '0' && c <= '9' ) x = (x<<1)+(x<<3)+(c^48) , c = getchar() ;
return x ;
}
int n , m ;
struct nn
{
int lst , to ;
}E[N<<1] ;
int head[N] , tot = 1 ;
inline void add( int x , int y )
{
E[++tot] = (nn){ head[x] , y } ;
head[x] = tot ;
}
struct nod
{
int x , y ;
}edge[N] ;
int dep[N] , dis[N] , fa[25][N] ;
void dfs( int now , int f )
{
dep[now] = dep[f] + 1 ;
fa[0][now] = f ;
for(int i = 1 ; i <= 20 ; i ++ ) fa[i][now] = fa[i-1][fa[i-1][now]] ;
for(int i = head[now] ; i ; i = E[i].lst ) {
int t = E[i].to ;
if( t == f ) continue ;
dfs( t , now ) ;
}
}
int LCA( int x , int y )
{
if( dep[x] < dep[y] ) swap( x , y ) ;
for(int i = 20 ; i >= 0 ; i -- ) {
if( dep[fa[i][x]] >= dep[y] ) {
x = fa[i][x] ;
}
}
if( x == y ) return x ;
for(int i = 20 ; i >= 0 ; i -- ) {
if( fa[i][x] != fa[i][y] ) x = fa[i][x] , y = fa[i][y] ;
}
return fa[0][x] ;
}
int ask( int x , int y ) { return dep[x]+dep[y]-2*dep[LCA(x,y)] ; }
int op[N] , id[N] ;
int bin[N] ; // 动态维护直径左右端点
struct P{ int u , v , dis ; }dia[N];
int Find( int x ) { return x==bin[x]?x:bin[x]=Find(bin[x]) ; }
void Merge( int x , int y )
{
int u1 = dia[x].u , v1 = dia[x].v , u2 = dia[y].u , v2 = dia[y].v ;
bin[y] = x ;
dia[x] = dia[x].dis>dia[y].dis ? dia[x] : dia[y] ;
dia[x] = dia[x].dis>ask(u1,u2) ? dia[x] : (P){ u1 , u2 , ask(u1,u2) } ;
dia[x] = dia[x].dis>ask(u1,v2) ? dia[x] : (P){ u1 , v2 , ask(u1,v2) } ;
dia[x] = dia[x].dis>ask(v1,u2) ? dia[x] : (P){ v1 , u2 , ask(v1,u2) } ;
dia[x] = dia[x].dis>ask(v1,v2) ? dia[x] : (P){ v1 , v2 , ask(v1,v2) } ;
}
bool vis[N] ;
int ans[N] ;
int main()
{
n = read() , m = read() ;
int x , y ;
for(int i = 1 ; i < n ; i ++ ) {
scanf("%d%d" , &x , &y ) ;
add( x , y ) ; add( y , x ) ;
bin[i] = i , dia[i] = (P){i,i,0} ;
edge[i].x = x , edge[i].y = y ;
}
bin[n] = n ;
dia[n] = (P){n,n,0} ;
dep[0] = -1 ;
dfs( 1 , 0 ) ;
for(int i = 1 ; i <= m ; i ++ ) {
scanf("%d%d" , &op[i] , &id[i] ) ;
if( op[i] == 1 ) vis[id[i]] = 1 ;
}
for(int i = 1 ; i < n ; i ++ ) {
if( !vis[i] ) {
int fx = Find( edge[i].x ) , fy = Find( edge[i].y ) ;
if( fx != fy ) Merge( fx , fy ) ;
}
}
int len = 0 ;
for(int i = m ; i >= 1 ; i -- ) {
if( op[i] == 1 ) {
Merge( Find(edge[id[i]].x) , Find(edge[id[i]].y) ) ;
}
else {
int rt = Find( id[i] ) ;
ans[++len] = max( ask( id[i] , dia[rt].u ) , ask( id[i] , dia[rt].v ) ) ;
}
}
for(int i = len ; i >= 1 ; i -- ) {
printf("%d\n" , ans[i] ) ;
}
return 0 ;
}
T14 贪心 / 构造 / 按位枚举
首先一定要强调:符号变反指的只是正数变负数、负数变正数即可,不需要恰好相反数!!!
既然出现了 a n d and and 运算,我们应该考虑 按位枚举
从前我们知道,遇到 X O R XOR XOR 求极值的问题 应该从最高位枚举,贪心考虑最高位贡献
而这道题告诉我们另一个技巧: a n d and and 运算要考虑最低位!!!
基于 a n d and and 运算最典型的性质:一个为 0 0 0 则结果为 0 0 0
试想,如果我们以每个 m a s k mask mask 的最高位为依据,将所有的 m a s k mask mask 分成 log 类
从最低位开始枚举,当我们枚举到第 i i i 位,第 i i i 位做出的选择对前 i − 1 i-1 i−1 类是 没有影响的 (前 i − 1 i-1 i−1 类在这一位上都是 0 )
什么意思?就是后面的决策对于前面的没有影响,满足了 无后效性
(其实,按每一个数的最低位划分也是可以的,只要保证先枚举的有一串连续 0 0 0 ,使得后面枚举的对这些 0 无贡献,核心还是优化枚举顺序构造 无后效性)
那这样我们就可以做很多事情了
对于本题,不妨设 s u m > 0 sum>0 sum>0 ,我们枚举时就可以考虑分组后,能否使得 当前这一类的贡献 小于 0
假设 第 i i i 位填 0 ,计算这一类此时的初始贡献 p s u m psum psum,若 小于 0 ,我们就让它填 0 ,并把这个影响施加到 n n n 个数上
反之,因为这一类的数都满足,在第 i i i 位上是 1 1 1(分类的另一个好处),我们就可以在第 i i i 位填 1 1 1 来 改变每个数 1 1 1 的个数的奇偶性
那么 p s u m psum psum 自然也会变为 其相反数
并且这个填 0 0 0 还是 1 1 1 的选择 对 前 i − 1 i-1 i−1 位,已经配出来的 p s u m < 0 psum<0 psum<0 是没有影响的
因此我们就做到了让每一类中的数贡献都为负数,总体加起来当然也为负数
但还有一个小细节:若 p s u m = 0 psum=0 psum=0 ?
填
1
1
1 ?这里给出一组
h
a
c
k
hack
hack ,
我们可以证明:填
0
0
0 则一定能够保证有解
从上面这组数据我们发现,填 1 1 1 有可能使得 不仅当前位贡献为0,还可能导致更高位的贡献变成0
注意这里为什么用词 变成,题目中有一个限制条件: ∑ v a l i ≠ 0 \sum val_i \neq 0 ∑vali=0
这意味着,高位中至少是有一类的 p s u m psum psum 是不为 0 0 0 的
如果我们将低位全部填成 0 0 0,那么当枚举到 第一个 p s u m ≠ 0 psum \neq 0 psum=0 的位置时,这一类中的数,此时的 m a s k mask mask 就都是 ( 1000... ) 2 (1000...)_2 (1000...)2了
因此这一位的决策可以整体修改这一位的所有 v a l val val 值,进而也必然能修改 v a l val val 值