「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\) 必须改变的次数,因此可以简单画图:
上图描述了值域上的分布情况。其中 \(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)\)。
小结:
- 本题中最突出的就是剪枝技巧的运用。对于 \(\min,\max\) 这样信息的维护,我们只需要保证答案可以被找到,而不需要保证状态集合和实际计算的集合是一致的,这就可以帮助我们修改计算集合以简化运算。
- 可以学习一下复杂度分析的方法。
代码
#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;
}