ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

「NOI2022」冒泡排序

2022-09-02 21:03:47  阅读:266  来源: 互联网

标签:le 限制 int 冒泡排序 NOI2022 const Property 逆序


题目

给定正整数 \(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.

这里其实体现了结论的两种用法:

  1. 限制解的形态,从这个角度入手我们得到了 DP。

  2. 放松计算限制,从这个角度入手我们得到了贪心,并且相对来说实现更加简单。

第一个用法比较直接,比较好想。第二个用法可能需要绕一个弯子,想起来有难度,但是不能忘记这种思路。寻找结论时也可以从这两个方向入手。

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有