「CF765F」Souvenirs

题目

点这里看题目。

分析

蛮巧妙的一道题目。

首先,虽然这个问题看起来有很明显的分块特征,但是我们可以对问题进行离线,并使用常用技巧——扫描右端点,维护左端点的一些信息。直接维护答案明显过于复杂,我们可以维护每个单点的贡献,然后区间求 \(\min\)

具体来说,当扫描到 \(r\) 的时候,对于 \(1\le l\le r\),我们需要维护好 \(c_l=\min_{l<k\le r}|a_k-a_l|\),这样区间求 \(\min\) 即可得到正确答案。

但是,直接维护 \(c_l\) 还是显得过于复杂。由于在询问的时候,右端点不变,所以我们只需要保证区间求 \(\min\) 的结果一定正确。当我们移动 \(r\),尝试用 \(a_r\) 去更新之前的 \(c_l\) 的时候,如果发现 \(\min_{l<k\le r}c_k\) 不大于更新过的 \(c_l\),那么继续更新就是没有意义的,因为此时区间求 \(\min\) 并不会导致 \([l,r]\) 的答案发生变化。

现在具体考虑如何维护。如果我们使用线段树维护 \(c\) 的区间最值,那么我们应当优先更新右子树。如果右子树更新完后,右子树的最小值不劣于左子树更新过后的信息,那么此时再进入左子树继续更新就是毫无意义的,我们即可剪枝退出。为了快速算出 \(a_r\) 对于某个区间的贡献,我们可以在区间上维护该区间的 \(a\) 排好序后的序列,也就是所谓“归并树”。


分析一下复杂度。我们关心的就是每个 \(l\)\(c_l\) 必须改变的次数,因此可以简单画图:

souvenir.png

上图描述了值域上的分布情况。其中 \(i<j\),且 \(j\) 是令 \(|a_k-a_i|=c_i\) 的最小的 \(k\),而黑色的线表示 \(\frac{a_i+a_j}{2}\)

此时,如果 \(a_r\le \frac{a_i+a_j}{2}\),那么 \(a_i\) 一定会被更新,并且 \(c_i\) 会折半。另一方面,如果 \(a_r>\frac{a_i+a_j}{2}\),则 \(a_i\) 不需要更新,因为根据我们的优化方法,\(a_j\) 的更新足以让 \([i,r]\) 取到正确的答案;因此每次修改可以使 \(c_i\) 折半,修改次数即 \(O(\log a)\)

总复杂度应为 \(O(n\log^2n\log a)\)

小结:

  1. 本题中最突出的就是剪枝技巧的运用。对于 \(\min,\max\) 这样信息的维护,我们只需要保证答案可以被找到,而不需要保证状态集合和实际计算的集合是一致的,这就可以帮助我们修改计算集合以简化运算。
  2. 可以学习一下复杂度分析的方法。

代码

#include <cstdio>
#include <vector>
#include <algorithm>

#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

const int INF = 0x3f3f3f3f;
const int MAXN = 3e5 + 5;

template<typename _T>
void read( _T &x )
{
	x = 0; char s = getchar(); bool f = false;
	while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
	while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
	if( f ) x = -x;
}

template<typename _T>
void write( _T x )
{
	if( x < 0 ) putchar( '-' ), x = -x;
	if( 9 < x ) write( x / 10 );
	putchar( x % 10 + '0' );
}

template<typename _T>
_T MIN( const _T a, const _T b )
{
	return a < b ? a : b;
}

std :: vector<int> que[MAXN];
std :: vector<int> seq[MAXN << 2];

int mn[MAXN << 2], cur;

int qL[MAXN], ans[MAXN];

int A[MAXN];
int N, M;

inline void Upt( const int x ) { mn[x] = MIN( mn[x << 1], mn[x << 1 | 1] ); }

void Build( const int x, const int l, const int r )
{
	if( l > r ) return ; mn[x] = INF;
	if( l == r ) { seq[x].push_back( A[l] ); return ; }
	int mid = ( l + r ) >> 1;
	Build( x << 1, l, mid );
	Build( x << 1 | 1, mid + 1, r );
	seq[x].resize( r - l + 1 );
	std :: merge( seq[x << 1].begin(), seq[x << 1].end(),
				  seq[x << 1 | 1].begin(), seq[x << 1 | 1].end(),
				  seq[x].begin() );
}

void Update( const int x, const int l, const int r, const int segL, const int segR, const int nVal )
{
	if( segL > segR ) return ;
	if( segL <= l && r <= segR )
	{
		std :: vector<int> :: iterator it = std :: upper_bound( seq[x].begin(), seq[x].end(), nVal );
		if( it != seq[x].end() ) mn[x] = MIN( mn[x], *it - nVal );
		if( it != seq[x].begin() ) mn[x] = MIN( mn[x], nVal - *( -- it ) );
		if( cur <= mn[x] ) return ;
	}
	if( l == r ) return ( void ) ( cur = MIN( cur, mn[x] ) );
	int mid = ( l + r ) >> 1;
	if( mid  < segR ) Update( x << 1 | 1, mid + 1, r, segL, segR, nVal );
	if( segL <= mid ) Update( x << 1, l, mid, segL, segR, nVal );
	Upt( x ), cur = MIN( cur, mn[x] );
}

int Query( const int x, const int l, const int r, const int segL, const int segR )
{
	if( segL <= l && r <= segR ) return mn[x];
	int mid = ( l + r ) >> 1, ret = INF;
	if( segL <= mid ) ret = MIN( ret, Query( x << 1, l, mid, segL, segR ) );
	if( mid  < segR ) ret = MIN( ret, Query( x << 1 | 1, mid + 1, r, segL, segR ) );
	return ret;
}

int main()
{
	read( N );
	rep( i, 1, N ) read( A[i] );
	read( M );
	rep( i, 1, M )
	{
		int r;
		read( qL[i] ), read( r );
		que[r].push_back( i );
	}
	Build( 1, 1, N );
	rep( i, 1, N )
	{
		cur = INF, Update( 1, 1, N, 1, i - 1, A[i] );
		for( int j = 0 ; j < ( int ) que[i].size() ; j ++ )
			ans[que[i][j]] = Query( 1, 1, N, qL[que[i][j]], i );
	}
	rep( i, 1, M ) write( ans[i] ), putchar( '\n' );
	return 0;
}

posted @ 2021-10-15 21:42  crashed  阅读(106)  评论(0编辑  收藏  举报