模拟赛 —— 好题分享

T1 异或 / 性质 / 分治

题意:

给你一个数 n n n ,让你求 ∑ i = 1 n − 1 i   ⨁   ( n − i ) \sum_{i=1}^{n-1}i\ \bigoplus \ (n-i) i=1n1i  (ni)

其中 n ≤ 1 0 500 n \leq 10^{500} n10500

看到异或,首先想到按位考虑,简单打表,会发现一个性质:

n n n 为奇数,则 i ⨁ ( n − i ) i\bigoplus (n-i) i(ni) 在个位上对答案的贡献为 1

n n n 为偶数,则 i ⨁ ( n − i ) i\bigoplus (n-i) i(ni) 在个位上没有贡献

个位上的贡献是好求的

由此我们可以考虑将 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=1n1i  (ni)

  1. 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+1i)

    手写几项发现: 1 ⨁ 2 k   ,   2 ⨁ 2 k − 1 , . . . . , 2 k − 1 ⨁ 2 , 2 k ⨁ 1 1\bigoplus2k \ , \ 2\bigoplus2k-1 , .... , 2k-1\bigoplus2 , 2k\bigoplus1 12k , 22k1,....,2k12,2k1 首尾相同,可以结合,即:

     = 2 ∑ i = 1 k i ⨁ ( 2 k + 1 − i ) = 2\sum_{i=1}^{k}i\bigoplus (2k+1-i) =2i=1ki(2k+1i)

    那么 i i i 2 k + 1 − i 2k+1-i 2k+1i 必定一奇一偶,然而到底是哪个并不确定,那么更改求和的顺序

     = 2 ∑ i = 1 k 2 i ⨁ ( 2 k + 1 − 2 i ) = 2\sum_{i=1}^{k}2i\bigoplus (2k+1-2i) =2i=1k2i(2k+12i)

    这么看就可以单拎出个位的贡献了

     = 2 ∑ i = 1 k 2 i ⨁ ( 2 k − 2 i ) + 2 k = 2\sum_{i=1}^{k}2i\bigoplus (2k-2i) + 2k =2i=1k2i(2k2i)+2k

     = 4 ∑ i = 1 k i ⨁ ( k − i ) + 2 k = 4\sum_{i=1}^{k}i\bigoplus (k-i) + 2k =4i=1ki(ki)+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 =4i=1k1i(ki)+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=12k1i  (2ki)

    我们的目的是 直接清除掉每对数的个位 ,但这个操作对于奇偶数的修改是不同的,那么将奇偶分拎出来

    = ∑ 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=1k1(2i)  (2k2i) + i=1k(2i1)  (2k2i+1)

    左侧式子同上

    = 2 ∑ i = 1 k − 1 i   ⨁   ( k − i ) 2\sum_{i=1}^{k-1}i\ \bigoplus \ (k-i) 2i=1k1i  (ki)

    = 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(2i2)  (2k2i)

    = 2 ∑ i = 1 k ( i − 1 )   ⨁   ( k − i ) 2\sum_{i=1}^{k}(i-1)\ \bigoplus \ (k-i) 2i=1k(i1)  (ki)

    化为同样的形式:

    = 2 ∑ i = 0 k − 1 i   ⨁   ( k − i − 1 ) 2\sum_{i=0}^{k-1}i\ \bigoplus \ (k-i-1) 2i=0k1i  (ki1)

    = 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) 2i=1k2i  (ki1)+2(k1)+2(k1)

    = 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 2i=1k2i  (ki1)+4k4

    = 2 f ( k − 1 ) + 4 k − 4 2f(k-1)+4k-4 2f(k1)+4k4

  所以 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(k1)+4k4

复杂度分析:

n n n 为奇数时,搜索树只会单叉进入下一层;

n n n 为偶数时 n 2 \frac{n}{2} 2n n 2 − 1 \frac{n}{2}-1 2n1 相差不会太大,加上记忆化感性理解一下分出的两叉很快会汇合,同样状态不会太多

那么本题可过

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,k1] [ 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,k1]dp[k+1][r]C(rl,kl)

这样,最后删的点不同,确保了 方案不重 ;枚举所有可能被最后删除的点,确保了 不漏

我们做到了 不重不漏

此外,对断点 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| 21i=1nj=1nk=1np=1nik+jp

= n 2 ∑ i = 1 n ∑ k = 1 n ∣ i − k ∣ =n^2\sum_{i=1}^{n}\sum_{k=1}^{n}|i-k| =n2i=1nk=1nik

= 2 n 2 ∑ i = 1 n ∑ k = i n ( k − i ) =2n^2\sum_{i=1}^{n}\sum_{k=i}^{n}(k-i) =2n2i=1nk=in(ki)

= 2 n 2 ∑ i = 0 n − 1 i ( n − i ) =2n^2\sum_{i=0}^{n-1}i(n-i) =2n2i=0n1i(ni)

= 2 n 2 ∑ i = 1 n n i − i 2 =2n^2\sum_{i=1}^{n}ni-i^2 =2n2i=1nnii2

= 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(ni=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)(n1)

这是不考虑加入边的答案

接下来:

第一步,我们钦定每个点对都是从上往下走,这样可以把加入边分成两类:斜率大于 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 i1 类是 没有影响的 (前 i − 1 i-1 i1 类在这一位上都是 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 i1 位,已经配出来的 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值