• 「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.

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

    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;
    }
    
  • 相关阅读:
    android中statusbar高度的问题
    int和short做循环计数器时的效率问题
    解决Rectangle Packing问题【原创】
    10个android开源项目(转)
    自动编译.9.png文件
    通过wifi调试android程序
    HBase 性能优化笔记
    [转载]定制CentOS 6.3 自动安装盘
    region split时metascan出现regioninfo为空
    Google Dremel 原理 如何能3秒分析1PB
  • 原文地址:https://www.cnblogs.com/crashed/p/16651199.html
Copyright © 2020-2023  润新知