7.30 模拟赛总结 [神奇性质]+[线段树二分]

复盘

7:40 成功开题,艾教场

看 T1,感觉很神奇,但看数据范围没有开到 1 e 9 1e9 1e9 之类的,应该是得怎么搜一下;T2 发现做过原题,秒了;T3 第一眼没看懂题,推样例始终不理解其中的一句话;T4 听说是 arc 原,看起来应该可做

尝试 T1,爆搜给了 45pts,想了想可以把一段操作打包做,不以操作为阶段,而以每次往桶里倒水为阶段,这样可以在 O ( x y z ) O(xyz) O(xyz) 的复杂度内把所有边搜出来,最后再跑最短路

感觉很对,遂开写,20min 写的暴力跑出来了大样例,准备优化 (噩梦开始)

首先想到的操作是 ( z − y − . . . ) , ( z − x − . . . ) , ( y − x − . . . ) (z-y-...),(z-x-...),(y-x-...) (zy...),(zx...),(yx...),然后这会有个问题,比如说先灌满 z z z,然后不停往 x x x 里倒,最后得到 z − k x z-kx zkx 的一个东西,再倒进桶里。问题在于 最后一次 z z z 可以不清空 ,就是说可能留着 z z z 为满的情况下只使用 x , y x,y x,y 把桶灌到合适为止?那这样图得分层, f [ i ] [ 0 / 1 / 2 / 3 / 4 ] f[i][0/1/2/3/4] f[i][0/1/2/3/4] 表示能用 x y z / x y / x z / y z / z xyz/xy/xz/yz/z xyz/xy/xz/yz/z 的情况下的最小值

写写写,一顿分讨后代码长度来到 250 行,已经 1h 过去,但心想 T2 已经 100 了,不慌

发现样例没过,推了一下感觉样例错了???但爆搜跑出来的没问题啊???直接懵逼了

静态查错了大概 20min,未果…已经 10:15

决定先把 T2 写了,Kruskal 重构树+经典结论维护 dfn 最小最大的 LCA,十分顺利,样例一遍过,10:40 交了,发现这题没卡 l o g 2 log^2 log2 做法,可惜

回看 T1,仍觉得样例不对

看 T3,发现题根本看不懂?不懂样例解释里说 “1、4不能” 通信是什么意思,明明可以选 4 4 4 啊?无奈只能跳过

先看 T4 吧,想 q n qn qn 做法瞪出一个性质:必定选一段长度为 1 1 1,很快把暴力写完了,只剩 40min 了!

再看 T1 ,发现那个样例是因为我看错了,滑动条拉到最右边导致我看的是后面几个答案…

难蚌,经过手玩已经找到之前的问题了,不仅可以 z − x − x − . . . z-x-x-... zxx... 还可以 x + . . + x − z x+..+x-z x+..+xz 得到新的,这样讨论完全是 2 3 = 8 2^3=8 23=8 种状态!

决定把之前的删掉重构一遍,在过程中算复杂度发现 8 ∗ 1 e 5 ∗ 300 ∗ l o g 8*1e5*300*log 81e5300log…心态有点崩

最后也是不出意料地没写完

35+100+0+40=175 , rk_ O ( n ! ) O(n!) O(n!)

懂了,艾教场一定要放弃 T1

T1 离正解很近了… 考虑把上面那种难处理地操作直接换到最后做,就可以直接背包了。推出来离谱做法后应该再尝试优化一下的…

T3 70pts 的主席树几乎是送的…(甚至实现好可以拿100),而我样例都没推懂…

结论:要学会放弃艾教的 T1

题解

T1

在这里插入图片描述
我们理想的状态当然是 每次往桶里倒水的操作互不影响,这样就可以 O ( x y z ) O(xyz) O(xyz) 处理后直接背包

可以证明,一定存在一种最优方案,会影响后面的操作只会在最后做一次

有影响指的是 “某个杯子不清空,在之后的操作中不再用到” ,不妨设为是 x x x 杯,由于操作顺序不固定,那么之后 y , z y,z y,z 的清空操作不妨直接放到前面完成

唯一的问题是如果在接下来又在 y , z y,z y,z 中执行了一次 “对后面有影响” 的操作呢?手玩后发现,这样的情况一定可以被另一种上文所说的方式代替掉!

那么做法就很简单:处理 b t [ x ] bt[x] bt[x] 表示往桶里倒 x x x 的水,且清空,最小操作次数; b t 2 [ x ] bt2[x] bt2[x] 表示不清空,最小次数

对于前者做完全背包,对于后者做分组背包

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 1e5+10 ;

int X , Y , Z , n ;
namespace Part1
{
	// 操作顺序可以随便交换,不妨就把填满某个不再动的操作放到最后 
	struct nn
	{
		int x , y , z , d ;
	};
	int D[101][101][101] , f[N] , bt[301] , bt2[301] ;// 最后一次操作 
	queue<nn> q ;
	void solve()
	{
		memset( D , -1 , sizeof D ) ;
		memset( bt , 0x3f , sizeof bt ) ;
		memset( bt2 , 0x3f , sizeof bt2 ) ;
		q.push({0,0,0,0}) ;
		D[0][0][0] = 0 ;
		while( !q.empty() ) {
			int x=q.front().x,y=q.front().y,z=q.front().z,d=q.front().d;
			q.pop() ;
			bt[x+y+z] = min( bt[x+y+z] , d+(x!=0)+(y!=0)+(z!=0) ) ;
			bt2[x] = min(bt2[x],d+(x!=0)); bt2[y] = min(bt2[y],d+(y!=0)); bt2[z] = min(bt2[z],d+(z!=0)) ;
			bt2[x+y]=min(bt2[x+y],d+(x!=0)+(y!=0)),bt2[x+z]=min(bt2[x+z],d+(x!=0)+(z!=0)),bt2[y+z]=min(bt2[y+z],d+(y!=0)+(z!=0)) ;
			if( D[X][y][z] == -1 ) D[X][y][z] = d+1 , q.push({X,y,z,d+1}) ;
			if( D[x][Y][z] == -1 ) D[x][Y][z] = d+1 , q.push({x,Y,z,d+1}) ;
			if( D[x][y][Z] == -1 ) D[x][y][Z] = d+1 , q.push({x,y,Z,d+1}) ;
			if( D[0][y][z] == -1 ) D[0][y][z] = d+1 , q.push({0,y,z,d+1}) ;
			if( D[x][0][z] == -1 ) D[x][0][z] = d+1 , q.push({x,0,z,d+1}) ;
			if( D[x][y][0] == -1 ) D[x][y][0] = d+1 , q.push({x,y,0,d+1}) ;
			//x->y 
			int tim=min(x,Y-y); if( D[x-tim][y+tim][z]==-1 ) D[x-tim][y+tim][z]=d+1 , q.push({x-tim,y+tim,z,d+1}) ;
			tim = min(x,Z-z); if( D[x-tim][y][z+tim]==-1 ) D[x-tim][y][z+tim]=d+1 , q.push({x-tim,y,z+tim,d+1}) ;
			tim = min(y,X-x); if( D[x+tim][y-tim][z]==-1 ) D[x+tim][y-tim][z]=d+1 , q.push({x+tim,y-tim,z,d+1}) ;
			tim = min(y,Z-z); if( D[x][y-tim][z+tim]==-1 ) D[x][y-tim][z+tim]=d+1 , q.push({x,y-tim,z+tim,d+1}) ;
			tim = min(z,X-x); if( D[x+tim][y][z-tim]==-1 ) D[x+tim][y][z-tim]=d+1 , q.push({x+tim,y,z-tim,d+1}) ;
			tim = min(z,Y-y); if( D[x][y+tim][z-tim]==-1 ) D[x][y+tim][z-tim]=d+1 , q.push({x,y+tim,z-tim,d+1}) ;
		}
		memset( f , 0x3f , sizeof f ) ;
		f[0] = 0 ;
		for(int i = 1 ; i <= X+Y+Z ; i ++ ) {
			if( bt[i]>1000 ) continue ;
			for(int j = i ; j <= n ; j ++ ) { // 完全背包 
				f[j] = min( f[j] , f[j-i]+bt[i] ) ;
			}
		}
		for(int j = n ; j >= 1 ; j -- ) {
			for(int i = 1 ; i <= min(j,X+Y+Z) ; i ++ ) {
				if( bt2[i] > 1000 ) continue ;
				f[j] = min( f[j] , f[j-i]+bt2[i] ) ; 
			}
		}
		for(int i = 1 ; i <= n ; i ++ ) printf("%d " , f[i]>1e8?-1:f[i] ) ;
	}
}

int main()
{
	scanf("%d%d%d%d" , &X , &Y , &Z , &n ) ;
	if( X > Y ) swap( X , Y ) ;
	if( Y > Z ) swap( Y , Z ) ;
	if( X > Y ) swap( X , Y ) ;
	Part1::solve() ;
	return 0 ; 
}

T2

在这里插入图片描述
典题

T3

在这里插入图片描述
考虑确定 P P P 后,每个节点 z z z 的负载是多少,等价于查以它为根的各个子树中的 s i z siz siz

那么二分答案后 check ,check 方法很多,已知的:线段树合并/主席树 在原序列上做;按 w i w_i wi 排序后离线树状数组做

但这样不可避免二分答案的 l o g log log + 检验的 l o g log log

我们之前遇到这个问题是在 区间第 K 大,当时利用了线段树二分来解决,我们考虑同样编一个线段树二分的做法

首先对于每个节点还是要维护子树中 w i w_i wi 的值域,通过 d f n dfn dfn 序转化到序列上 前缀主席树实现

接下来直接在查询的过程中二分,取出若干个主席树的根节点,每次计算左子树的答案,判断后 整体 往左/右儿子走

口胡很简单,细节不少

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 3e5+10 , INF = 1e6 ;

int n , w[N] ;
LL K ;
vector<int> E[N] ;
int dfn[N] , tim , ed[N] , nam[N] ;
void dfs( int x , int fa )
{
	dfn[x] = ++tim ; nam[tim] = x ;
	for(int t : E[x] ) {
		if( t == fa ) continue ;
		dfs( t , x ) ; 
	}
	ed[x] = tim ;
}
struct Segtree
{
	int ls , rs , sum ;
}t[32*N] ;
int tot , rt[N] ;
inline int build() { return ++tot ; }
int Insert( int p , int l , int r , int x )
{
	int nw = build() ;
	t[nw] = t[p] ;
	if( l == r ) {
		t[nw].sum ++ ;
		return nw ;
	}
	int mid = (l+r)>>1 ;
	if( x <= mid ) t[nw].ls = Insert( t[p].ls , l , mid , x ) ;
	else t[nw].rs = Insert( t[p].rs , mid+1 , r , x ) ;
	t[nw].sum = t[t[nw].ls].sum + t[t[nw].rs].sum ;
	return nw ;
}
int p[N] , q[N] , Sum[N] , len , X , S , R ;//q-p
LL calc( int mid )
{
	int siz = 0 ; LL sum = 0 ;
	if( w[X] <= mid ) siz = 1 ;
	for(int i = 1 ; i <= len ; i ++ ) { 
		int v = t[q[i]].sum - t[p[i]].sum + Sum[i] ;
		sum += 1LL*siz*v ;
		siz += v ;
	}
	int v = t[R].sum-siz+S ;
	sum += 1LL*siz*v ;
	siz += v ; 
	return sum ;
}
int ask( int l , int r )
{
	if( l == r ) {
		if( calc(l) < K ) return l ;// 需要再算一次,不然边界会错
		return l-1 ;
	}
	int mid = (l+r)>>1 ;
	// 选左子树答案 
	int siz = 0 ; LL sum = 0 ;
	if( w[X] <= mid ) siz = 1 ;
	for(int i = 1 ; i <= len ; i ++ ) { 
		int v = t[t[q[i]].ls].sum - t[t[p[i]].ls].sum + Sum[i] ;
		sum += 1LL*siz*v ;
		siz += v ;
	}
	int v = t[t[R].ls].sum-siz+S ;
	sum += 1LL*siz*v ;
	siz += v ; 
	if( sum < K ) { //可以更大,往右儿子走 
		for(int i = 1 ; i <= len ; i ++ ) {
			Sum[i] += t[t[q[i]].ls].sum-t[t[p[i]].ls].sum ;// 需要补前缀
			q[i] = t[q[i]].rs ; p[i] = t[p[i]].rs ;
		}
		S += t[t[R].ls].sum ;
		R = t[R].rs ;
		return ask( mid+1 , r ) ;
	}
	else { // 往左儿子走 
		for(int i = 1 ; i <= len ; i ++ ) {
			q[i] = t[q[i]].ls ; p[i] = t[p[i]].ls ;
		}
		R = t[R].ls ;
		return ask( l , mid ) ;
	}
}
void solve()//把二分放到主席树上 
{
	int Min = 1e6 ;
	for(int x = 1 ; x <= n ; x ++ ) {
		X = x ; S = 0 ; R = rt[n] ; len = 0 ; 
		for(int t : E[x] ) {
			if( dfn[t] < dfn[x] ) continue ;
			len ++ ;
			p[len] = rt[dfn[t]-1] ; q[len] = rt[ed[t]] ; Sum[len] = 0 ;//这一层的根全存起来,整体走
		}
		Min = min( Min , ask(0,INF)-w[x] ) ;
	}
	printf("%d\n" , Min ) ;
}

int main()
{
	scanf("%d%lld" , &n , &K ) ;
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &w[i] ) ;
	}
	int x , y ;
	for(int i = 1 ; i < n ; i ++ ) {
		scanf("%d%d" , &x , &y ) ;
		E[x].push_back( y ) ;
		E[y].push_back( x ) ;
	}
	dfs( 1 , 0 ) ; // 需要子树信息,除了线段树合并之外,还可以通过 dfn 搞成前缀主席树 
	rt[0] = build() ;
	for(int i = 1 ; i <= n ; i ++ ) {
		rt[i] = Insert( rt[i-1] , 0 , INF , w[nam[i]] ) ;
	}
	solve() ;
	return 0 ; 
}

类似于 树套树求区间第 K K K 大时,整体往下走的思路

T4


好题,不过似乎比较套路

枚举 其中一段后 后,只有两种情况

在这里插入图片描述
这是由于,后边的两段中 有一段内部最大值更大,把这一段尽可能延申一定不差

( bb几句:好吧赛场上没有继续推下去,所以遇到这种情况我们要最大限度利用贪心简化条件

那么同理对于左边的两段,我们在比较二者内部的最大值后也可以进行延申

总之,最后只会存在两种情况:

[ L , L ] , [ L + 1 , R − 1 ] , [ R , R ] [L,L],[L+1,R-1],[R,R] [L,L],[L+1,R1],[R,R]

[ L , t − 1 ] , [ t , t ] , [ t + 1 , R ] [L,t-1],[t,t],[t+1,R] [L,t1],[t,t],[t+1,R]

第一种直接算就行,对于第二种情况:

首先它有三个变量,不好搞

对于区间 M a x Max Max 可以有一种想法:由于一定有一段能够取到整个区间的 M a x Max Max我们考虑它的位置,通过 下标在某一区间内 把某个变量给定下来

具体来说,假设区间最大值的位置为 p o s pos pos:(有多个取最左边)

不妨认为 t < p o s t<pos t<pos,那么第三段的答案就是固定的 M a x Max Max,接下来只需要求 m a x ( a L , . . . , a t − 1 ) + a t max(a_L,...,a_{t-1})+a_t max(aL,...,at1)+at 的最大值

与下标有关,我们按 L L L 扫描线,线段树下标 t t t 位置维护的就是 t t t 的答案

L − − L-- L,对线段树的影响是修改了某一段的区间最大值,通过 单调栈找到影响的范围 + 线段树区间修改 解决;然后新插入 L L L 位置的答案也是好做的

对于一个询问,只需查 [ L + 1 , p o s − 1 ] [L+1,pos-1] [L+1,pos1] 内部的答案即可, p o s pos pos 是区间最大值位置

对于 t > p o s t>pos t>pos 只需要把序列翻转后做一遍即可

#include<bits/stdc++.h>
using namespace std ;

typedef long long LL ;
const int N = 3e5+100 ;

int n , Q , a[N] , st[20][N] , id[20][N] , Lg2[N] ;
void make_st()
{
	for(int i = 2 ; i <= n ; i ++ ) Lg2[i] = Lg2[i/2]+1 ;
	for(int i = 0 ; i <= 18 ; i ++ ) {
		if( i == 0 ) {
			for(int j = 1 ; j <= n ; j ++ ) st[i][j] = a[j] , id[i][j] = j ;
		}
		else {
			for(int j = 1 ; j <= n-(1<<i)+1 ; j ++ ) {
				if( st[i-1][j] >= st[i-1][j+(1<<(i-1))] ) { // 多个 Max,取最靠左 
					st[i][j] = st[i-1][j] ;
					id[i][j] = id[i-1][j] ;
				}
				else {
					st[i][j] = st[i-1][j+(1<<(i-1))] ;
					id[i][j] = id[i-1][j+(1<<(i-1))] ;
				}
			}
		}
	}
}
typedef pair<int,int> PII ;
PII ask( int l , int r )
{
	int k = Lg2[r-l+1] ;
	if( st[k][l] >= st[k][r-(1<<k)+1] ) {
		return {st[k][l],id[k][l]} ;
	}
	return {st[k][r-(1<<k)+1],id[k][r-(1<<k)+1]} ;
}
// 推性质,简化条件
// 对于 Max,很常见的处理是考虑最大值位置,转化成某一段区间内某个值是一定的,减少变量 
struct nn
{
	int l , r , id , Mx , pos ;
}q[N] ;
bool cmp( nn x , nn y )
{
	return x.l > y.l ;
}
int ans[N] , stk[N] , top ;
struct Segtree
{
	int l , r , Max , Val , Mx ;//单点最小值/区间最大值/和的最小值 
	int tag ; 
}t[4*N] ;
inline void update( int p )
{
	t[p].Mx = min( t[p<<1].Mx , t[p<<1|1].Mx ) ;
	t[p].Max = min( t[p<<1].Max , t[p<<1|1].Max ) ;
}
void build( int p , int l , int r )
{
	t[p].l = l , t[p].r = r , t[p].Max = t[p].Val = t[p].Mx = t[p].tag = 0 ;
	if( l == r )  {
		t[p].Max = a[l] ;
		t[p].Mx = a[l] ;
		return ;
	}
	int mid = (t[p].l+t[p].r)>>1 ;
	build( p<<1 , l , mid ) ; build( p<<1|1 , mid+1 , r ) ;
	update( p ) ;
}
inline void spread( int p )
{
	if( t[p].tag ) { // 强行让后来的覆盖前面的 
		int d = t[p].tag ; t[p].tag = 0 ;
		t[p<<1].Val = d ; t[p<<1].tag = d ; t[p<<1].Mx = t[p<<1].Max+t[p<<1].Val ;
		t[p<<1|1].Val = d ; t[p<<1|1].tag = d ; t[p<<1|1].Mx = t[p<<1|1].Max+t[p<<1|1].Val ;
	}
}
void change( int p , int l , int r , int x ) //[l,r]内的区间最大值更改为 x 
{
	if( l <= t[p].l && t[p].r <= r ) {
		t[p].Val = x ; t[p].tag = x ;
		t[p].Mx = t[p].Max+t[p].Val ;
		return ;
	}
	spread(p) ;
	int mid = (t[p].l+t[p].r)>>1 ;
	if( l <= mid ) change( p<<1 , l , r , x ) ;
	if( r > mid ) change( p<<1|1 , l , r , x ) ;
	update( p ) ;
}
int query( int p , int l , int r ) // [l,r] 内和的最大值 
{
	if( l <= t[p].l && t[p].r <= r ) {
		return t[p].Mx ;
	}
	spread(p) ;
	int mid = (t[p].l+t[p].r)>>1 , res = 1e9 ;
	if( l <= mid ) res = min( res , query(p<<1,l,r) ) ;
	if( r > mid ) res = min( res , query(p<<1|1,l,r) ) ;
	return res ; 
}
void solve()
{
	sort( q+1 , q+Q+1 , cmp ) ;
	build( 1 , 1 , n ) ;
	stk[0] = n ;
	for(int i = n , j = 1 ; i >= 1 ; i -- ) {
		while( top && a[i]>a[stk[top]] ) top -- ;
		int R = stk[top] ;
		stk[++top] = i ;
		if( i+1 <= R ) {
			change( 1 , i+1 , R , a[i] ) ;
		}
		while( j <= Q && q[j].l == i ) {
			if( i+1<=q[j].pos-1 ) {
				ans[q[j].id]=min(ans[q[j].id],query(1,i+1,q[j].pos-1)+q[j].Mx ) ;
			}
			j ++ ;
		}
	}
}

int main()
{
	scanf("%d%d" , &n , &Q ) ;
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &a[i] ) ;
	}
	make_st() ;
	for(int i = 1 ; i <= Q ; i ++ ) {
		scanf("%d%d" , &q[i].l , &q[i].r ) ;
		q[i].id = i ;
		PII x = ask(q[i].l,q[i].r) ;
		q[i].Mx = x.first , q[i].pos = x.second ;
		ans[i] = a[q[i].l]+a[q[i].r]+ask(q[i].l+1,q[i].r-1).first ;
	}
	solve() ;
	reverse( a+1 , a+n+1 ) ;
	for(int i = 1 ; i <= Q ; i ++ ) {
		q[i].l = n-q[i].l+1 , q[i].r = n-q[i].r+1 ;
		swap( q[i].l , q[i].r ) ;
		q[i].pos = n-q[i].pos+1 ;
	}
	solve() ;
	for(int i = 1 ; i <= Q ; i ++ ) {
		printf("%d\n" , ans[i] ) ;
	}
	return 0 ; 
}
  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值