「NOI2022」冒泡排序
作者:互联网
题目
给定正整数 \(n\) 和 \(m\) 条限制,每条限制为非负整数三元组 \((L,R,V)\)。
现在,你需要构造一个长度为 \(n\) 的非负整数序列,并且满足每一条限制:一条限制 \((L,R,V)\) 表示你所构造的序列必须满足 \(\min_{L\le i\le R}a_i\) 恰好为 \(V\)。此外,你还需要最小化逆序对数。
输出最小逆序对数。多组数据。
对于 \(100\%\) 的数据,满足 \(\sum n,\sum m\le 10^6,1\le L\le R\le n,0\le V\le 10^9\)。
此处额外补充若干特殊性质,供参考。
Property A:\(V\in \{0,1\}\)。
Property B:\(L=R\)。
Property C:所有限制的 \([L,R]\) 两两不相交。
分析
确实是很好的题目,考场上就可以想出来的我只能顶礼膜拜。
下面从特殊性质入手来分析这道题目。
暴力
一时半会儿连暴力都写不出来,这是为什么呢?
不限制 \(a\) 的取值怎么写暴力嘛。怎么限制 \(a\) 的取值?将值域按照出现了的 \(V\) 分段(每一个 \(V\) 需要单独成段)。则每一段内的 \(a\) 显然应该取到同一个值;进一步地,把不包含出现过的 \(V\) 的段合并到相邻的 \(V\) 值上去明显不劣,因此得出结论:
Conclusion.
必然存在一个最优解,其中 \(\{a\}\) 中每一个元素的值都是出现过的 \(V\) 值。
现在可以完成 28pts 的暴力。良心!
Property A.
基于 Property A.,我们可以做一个暴力的计算。
首先,\(V=1\) 的限制就意味着 \(i\in [L,R]\) 的 \(a_i\) 都必须是 \(1\)。那么,我们尝试在剩下的 \(a\) 的位置上放 \(0\),可以设计 DP:\(f_{i,j}\) 表示前缀 \([1,i]\) 中,放了 \(j\) 个 \(0\),且 \(a_i=0\) 的最小逆序对数。转移需要注意,相邻两个 \(0\) 之间不能包含一个完整的 \(V=0\) 的限制,这个可以通过处理前缀最值限制转移达成。
进一步地,我们可以按照 \(j\) 这一维划分 DP 阶段。每一阶段的 DP 可以使用单调队列优化,因此复杂度可以优化到 \(O(n^2)\)。
后续应该可以接着优化,但是我考场上没有想出来所以不准备讲。
Property B.
不知道怎么做?猜!
此时的限制就是钦定某些 \(a\) 的值为特定值。那怎么猜?肯定是猜剩下的序列是单调的啊:
Conclusion.
最优解必然满足自由选取的 \(a\) 构成的子序列单调不降。
Proof.
如果在自由的值中,出现了逆序对 \(i<j,a_i>a_j\),我们尝试交换。
发现交换之后 \(a_i\) “凭空”变小、\(a_j\) “凭空”变大,而真正导致逆序对数目变化的是 \([i,j]\) 之间的对,所以交换之后肯定不会变劣。
我们当然可以设计 DP,用 \(f_{i,j}\) 表示前缀 \([1,i],a_i=j\) 的最小逆序对。暴力转移是 \(O(nm)\) 的,后续优化可以做到 \(O(n\log m)\)。
Note.
注意将费用计算完整。DP 的代价需要考虑所有已经确定的值和它构成的逆序对,不然答案会变小。
话说如果发现输出比答案小,不应该怀疑自己写错了吗?
但是,我们也可以贪心地看:设 \(I\) 为已确定的下标集合,\(c_{i,j}=\sum_{k\in I,k<i}[a_k>j]+\sum_{k\in I,k>i}[j>a_k]\),则我们令 \(a_i\in \arg\min_jc_{i,j}\)。这必然是一个下界;而如果出现了自由值的逆序对,我们可以交换消去,因此它一定可以被取到。这样就容易做到 \(O(n\log m)\) 了 。
Remark.
这里其实体现了结论的两种用法:
限制解的形态,从这个角度入手我们得到了 DP。
放松计算限制,从这个角度入手我们得到了贪心,并且相对来说实现更加简单。
第一个用法比较直接,比较好想。第二个用法可能需要绕一个弯子,想起来有难度,但是不能忘记这种思路。寻找结论时也可以从这两个方向入手。
Property C.
不知道怎么做?猜!
首先尝试向 Property B. 靠齐,那就可以先将每个 \((L,R,V)\) 的 \(V\) 放到 \(a_L\) 上,根据性质不用担心多个限制打架的问题。现在,我们相当于是钦定了若干个位置的值,并且位置 \(i\) 的 \(a_i\) 必须大于等于某个下界 \(l_i\)。
有了下界限制,我们无法轻易地进行交换。还是先将自由值逆序对 \(i<j,a_i>a_j\) 拿来考虑:如果 \(l_i>a_j\),则 \(i,j\) 之间必然会出现逆序对;否则 \(l_i\le a_j\),又由于 \(a_i>a_j\ge l_j\),交换仍然可以进行。
此时我们可以想到拓展 Property B. 的做法:如果设 \(I\) 为已确定的位置的下标集合,\(d_{i,j}=c_{i,j}+\sum_{1\le k<i,k\not \in I}[l_k>j]\),则我们可以令 \(a_{i}\in \arg\min_jd_{i,j}\)。这仍然是一个下界;而此时未被考虑的逆序对必然形如 \(i<j,a_i>a_j\ge l_i\),我们仍然可以交换消去。这样还是容易做到 \(O(n\log m)\)。
正解
不知道怎么做?猜!
此时可能出现无解的情况。检查过程可以直接按照 \(V\) 从大到小进行,用一个并查集查询后继就可以完成。顺便,我们还可以在这个过程中处理出 \(l\) 来。
首先尝试向 Property C. 靠齐。我们按照 \(V\) 从大到小进行,这样不同的 \(V\) 的限制是相对独立的。如果此时,\(V\) 相同的限制的 \([L,R]\) 满足 Property C. 的话,我们就可以直接利用那种做法——钦定某些位置的值,然后开始贪心。
问题是,如果 \(V\) 相同的限制的 \([L,R]\) 有重叠,Property C. 的策略就失效了。退回来考虑,“钦定”的实质,就是要保证 \(V\) 作为区间最小值可以被取到。既然是要放一个区间最小值,我们自然要尽量往前放。这是一个和位置和限制都有关的条件,所以:
-
从位置来考虑,位置 \(i\) 最多只能保证一部分 \(V=l_i\) 的限制满足要求(因为 \(l_i\) 是 \(\max\) 出来的结果),这需要令 \(a_i=l_i\)。
-
从限制来考虑,“尽量往前放”可以很容易地转化成贪心语言:从后往前贪心,如果不放会破坏限制,我们就必须令 \(a_i=l_i\)。
如何检查会不会破坏限制?首先,对于限制 \((L,R,V)\),我们将 \([L,R]\) 收缩到 \([L',R']\),使得 \(L'\) 是原区间内第一个 \(l_i\le a_i\) 的位置,\(R'\) 类似。扫描过程中,我们维护 \(q_j\) 表示 \(V=j\) 且尚未放置最小值的限制中,\(L'\) 的最大值。在 \(i\) 处贪心时检查 \(q_j\) 和 \(i\) 的关系即可知道需不需要令 \(a_i=l_i\)。
为什么是对的?不知道,这下子真的是猜的了。之后进行 Property C. 的贪心就可以了。
Remark.
注意逐步推广、逐步在主体思路上做出修改的思路。
有的时候修改是显然的,比如 Property B. 到 Property C. 的修改;但是有的修改需要绕一个弯,比如正解的修正过程。这个时候要适当回退思路,要意识到当前的想法很可能是一个枝干,比如最开始我并没有延续“钦定”的方向,而仅仅是在贪心过程中顺便保证了一下最小值(当然这是殊途同归的)。不过,即便是重走一些路,也比被卡在枝干上要好。
代码
Note.
代码实现和上面的说法不太一样,因为代码是一边贪心一边完成“钦定”,所以需要倒着贪心,不过正确性应该没有问题。
#include <cstdio>
#include <vector>
#include <cassert>
#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 -- )
typedef long long LL;
const LL INF = 1e18;
const int inf = 1e9;
const int MAXN = 1e6 + 5;
template<typename _T>
inline void Read( _T &x ) {
x = 0; char s = getchar(); bool f = false;
while( ! ( '0' <= s && s <= '9' ) ) { 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>
inline void Write( _T x ) {
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) Write( x / 10 );
putchar( x % 10 + '0' );
}
template<typename _T>
inline _T Min( const _T &a, const _T &b ) {
return a < b ? a : b;
}
template<typename _T>
inline _T Max( const _T &a, const _T &b ) {
return a > b ? a : b;
}
struct Restriction {
int l, r, v;
Restriction(): l( 0 ), r( 0 ), v( 0 ) {}
Restriction( int L, int R, int V ): l( L ), r( R ), v( V ) {}
};
std :: pair<int, int> mn[MAXN << 2];
int tag[MAXN << 2];
int BIT[MAXN];
std :: vector<int> each[MAXN];
int lim[MAXN];
Restriction rstr[MAXN];
int fa[MAXN], low[MAXN];
int N, M, tot;
inline void Down( int &x ) { x &= x - 1; }
inline void Up( int &x ) { x += x & ( -x ); }
inline void Update( int x, int v ) { for( ; x <= tot ; Up( x ) ) BIT[x] += v; }
inline int Query( int x ) { int ret = 0; for( ; x ; Down( x ) ) ret += BIT[x]; return ret; }
inline void MakeSet( const int &n ) {
rep( i, 1, n ) fa[i] = i;
}
int FindSet( const int &u ) {
return fa[u] == u ? u : ( fa[u] = FindSet( fa[u] ) );
}
inline void UnionSet( const int &u, const int &v ) {
fa[FindSet( u )] = FindSet( v );
}
inline void Upt( const int &x ) {
mn[x] = Min( mn[x << 1], mn[x << 1 | 1] );
}
inline void Add( const int &x, const int &delt ) {
tag[x] += delt, mn[x].first += delt;
}
inline void Normalize( const int &x ) {
if( ! tag[x] ) return ;
Add( x << 1, tag[x] );
Add( x << 1 | 1, tag[x] );
tag[x] = 0;
}
void Build( const int &x, const int &l, const int &r ) {
if( l > r ) return ;
tag[x] = 0, mn[x] = { 0, - r };
if( l == r ) return ;
int mid = ( l + r ) >> 1;
Build( x << 1, l, mid );
Build( x << 1 | 1, mid + 1, r );
Upt( x );
}
void Update( const int &x, const int &l, const int &r, const int &segL, const int &segR, const int &delt ) {
if( l > r || segL > segR ) return ;
if( segL <= l && r <= segR ) { Add( x, delt ); return ; }
int mid = ( l + r ) >> 1; Normalize( x );
if( segL <= mid ) Update( x << 1, l, mid, segL, segR, delt );
if( mid < segR ) Update( x << 1 | 1, mid + 1, r, segL, segR, delt );
Upt( x );
}
std :: pair<int, int> QueryMin( 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; Normalize( x );
if( segR <= mid ) return QueryMin( x << 1, l, mid, segL, segR );
if( mid < segL ) return QueryMin( x << 1 | 1, mid + 1, r, segL, segR );
return Min( QueryMin( x << 1, l, mid, segL, segR ), QueryMin( x << 1 | 1, mid + 1, r, segL, segR ) );
}
int QuerySpec( const int &x, const int &l, const int &r, const int &p ) {
if( l == r ) return mn[x].first;
int mid = ( l + r ) >> 1; Normalize( x );
return p <= mid ? QuerySpec( x << 1, l, mid, p ) : QuerySpec( x << 1 | 1, mid + 1, r, p );
}
int main() {
int T; Read( T );
while( T -- ) {
Read( N ), Read( M );
rep( i, 1, M ) Read( rstr[i].l ), Read( rstr[i].r ), Read( rstr[i].v );
std :: sort( rstr + 1, rstr + 1 + M,
[] ( const Restriction &a, const Restriction &b ) -> bool {
return a.v > b.v;
} );
bool dead = false;
int old = -1; tot = 0;
per( i, M, 1 ) {
if( old ^ rstr[i].v ) tot ++;
old = rstr[i].v, rstr[i].v = tot;
}
MakeSet( N + 1 );
rep( i, 1, N ) low[i] = 1;
for( int l = 1, r ; l <= M ; l = r ) {
for( r = l ; r <= M && rstr[r].v == rstr[l].v ; r ++ );
for( int k = l ; k < r ; k ++ )
if( ( rstr[k].l = FindSet( rstr[k].l ) ) > rstr[k].r ) {
dead = true; break;
}
if( dead ) break;
for( int k = l ; k < r ; k ++ )
for( int x = FindSet( rstr[k].l ) ; x <= rstr[k].r ;
x = FindSet( x ) ) low[x] = rstr[k].v, UnionSet( x, x + 1 );
}
if( dead ) {
puts( "-1" ); continue;
}
rep( i, 1, N ) each[i].clear();
rep( i, 1, M ) each[rstr[i].r].push_back( i );
Build( 1, 1, tot );
rep( i, 1, tot ) BIT[i] = 0;
rep( i, 1, N ) Update( 1, 1, tot, 1, low[i] - 1, +1 );
LL ans = 0;
per( i, N, 1 ) {
Update( 1, 1, tot, 1, low[i] - 1, -1 );
for( const int &x : each[i] )
lim[rstr[x].v] = Max( lim[rstr[x].v], rstr[x].l );
if( i == lim[low[i]] ) {
ans += QuerySpec( 1, 1, tot, low[i] ) - Query( low[i] );
Update( 1, 1, tot, low[i] + 1, tot, +1 );
Update( low[i] + 1, +1 );
lim[low[i]] = 0;
} else {
std :: pair<int, int> tmp = QueryMin( 1, 1, tot, low[i], tot );
ans += tmp.first - Query( low[i] );
Update( 1, 1, tot, - tmp.second + 1, tot, +1 );
Update( - tmp.second + 1, +1 );
}
}
Write( ans ), putchar( '\n' );
}
return 0;
}
标签:le,限制,int,冒泡排序,NOI2022,const,Property,逆序 来源: https://www.cnblogs.com/crashed/p/16651199.html