雅礼集训10.21小结

雅礼集训10.21小结


前言:今天状态不是太好,一套题三道题几乎爆零。回过头来仔细一看,发现这些题并没有自己想象的这么难,现在我就贴下这三题的题目与题解。

T1:game

Description
小A和小B在玩一个游戏,他们两个人每人有n张牌,每张牌有一个点数,并且在接下来的n个回合中每回合他们两人会分别打出手中的一张牌,点数严格更高的一方得一分,然而现在小A通过某种神秘的方法得到了小B的出牌顺序,现在他希望规划自己的出牌顺序使得自己在得分尽可能高的前提下出牌的字典序尽可能大。

Input Format
第一行一个正整数n表示游戏进行的轮数。接下来一行n个整数,第i个数表示第i轮小B将要打出的牌的点数。接下来一行n个整数,表示小A拥有的牌的点数。

Output Format
输出一行n个整数,表示小A出牌的顺序。

Sample Input

5
1 2 3 4 5
3 2 2 1 4

Sample Output

2 3 4 2 1

Constraints
对于20%的数据,n ≤ 10对于40%的数据,n ≤ 3000对于60%的数据,n ≤ 6000对于100%的数据,n, a i a_i ai≤ 100000

简要思路:我们先不考虑字典序。这题主要做法就是用线段树来维护小A最多能得到的分数,然后在处理字典序时可以用贪心的方法,从前到后扫描,在不会减小小A能得到的分数的前提下尽可能地给当前点赋最大值,如果不行就适当减小赋值,不难看出赋值的单调性,可用二分法,只要再考虑是否得分即可。这时也正是依靠线段树快速判断当前贪心的可行性。
根据上面一段话,不难发现本题的重点与难点在于线段树的维护。鉴于本题的数值范围较小,再加上用数值作为区间的属性符合区间的加法特性,我们用数值来构建线段树。
我们可以用一棵线段树同时维护多个数据,其中 l [ n ] l[n] l[n]表示小A未成功对抗小B从而得分的卡牌数, r [ n ] r[n] r[n]表示小B未被对抗的卡牌数,而 s [ n ] s[n] s[n]表示小A的得分。进行区间合并时,由于线段树是按照数值构建的,位于左儿子区间的小B的卡牌一定会被右儿子区间的小A的卡牌成功对抗,至于小A能得多少分取决于两者中数目少的一方,另一方保留一点实力来服务于后面的统计。每次给一个点赋值并检验可行性时只要给线段树进行一次修改即可。时间复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)
详见代码和注释。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <set>
#include <algorithm>
#define N 100005
using namespace std;
int n , tot;
multiset<int> a;
int b[N];
inline int min( int a , int b ) {
	return a < b ? a : b;
}
struct semgenttree{
	#define ls root << 1
	#define rs root << 1 | 1
	int l[N << 2];
	int r[N << 2];
	int s[N << 2];
	inline void update( int root ) {
		int de = min( l[rs] , r[ls] );
		s[root] = s[ls] + s[rs] + de;
		l[root] = l[ls] + l[rs] - de;
		r[root] = r[ls] + r[rs] - de;
		return;
	}
	inline void insert( int root , int le , int ri , int pos , int vala , int valb ) {
		if ( le == ri ) {
			l[root] += vala;
			r[root] += valb;
			return;
		}
		int mid = ( le + ri ) >> 1;
		if ( pos <= mid ) {
			insert( ls , le , mid , pos , vala , valb );
		} else {
			insert( rs , mid + 1 , ri , pos , vala , valb );
		}
		update(root);
		return;
	}
}tree;//封装线段树方便搬运(手动滑稽~)
template < typename T >
inline void read( T & res ) {
	T pd = 1;
	res = 0;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
}
inline void getans( int p ) {
	tree.insert( 1 , 1 , n , b[p] , 0 , -1 );
	int le = b[p] + 1;
	int ri = *a.rbegin();
	while ( le < ri ) {
		int mid = ( le + ri + 1 ) >> 1;
		/*二分时可能得到一个原本小A就没有的卡牌数值,参见线段树的
		区间合并过程,处理不存在的数值会产生负数,减小tree.s[1]的值
		不会影响答案*/
		tree.insert( 1 , 1 , n , mid , -1 , 0 );
		if ( tree.s[1] + 1 == tot ) {
			le = mid;
		} else {
			ri = mid - 1;
		}
		tree.insert( 1 , 1 , n , mid , 1 , 0 );
	}
	//优先考虑得分,如果不行再考虑不得分
	tree.insert( 1 , 1 , n , le , -1 , 0 );
	if ( le <= ri && tree.s[1] + 1 == tot ) {
		tot--;
		printf("%d ",le);
		a.erase(a.find(le));
		return;
	}
	tree.insert( 1 , 1 , n , le , 1 , 0 );
	le = 1;
	ri = b[p];
	while ( le < ri ) {
		int mid = ( le + ri + 1 ) >> 1;
		tree.insert( 1 , 1 , n , mid , -1 , 0 );
		if ( tree.s[1] == tot ) {
			le = mid;
		} else {
			ri = mid - 1;
		}
		tree.insert( 1 , 1 , n , mid , 1 , 0 );
	}
	tree.insert( 1 , 1 , n , le , -1 , 0 );
	printf("%d ",le);
	a.erase(a.find(le));
	return;
}
int main () {
	freopen( "game.in" , "r" , stdin );
	freopen( "game.out" , "w" , stdout );
	read(n);
	for ( int i = 1 ; i <= n ; ++i ) {
		read(b[i]);
		tree.insert( 1 , 1 , n , b[i] , 0 , 1  ); 
	}
	int tem;
	for ( int i = 1 ; i <= n ; ++i ) {
		read(tem);
		tree.insert( 1 , 1 , n , tem , 1 , 0 );
		a.insert(tem);
	}
	tot = tree.s[1];
	for ( int i = 1 ; i <= n ; ++i ) {
		getans(i);
	}
	return 0;
}

T2:Time

Description
小A现在有一个长度为n的序列{ x i x_i xi},但是小A认为这个序列不够优美。小A认为一个序列是优美的,当且仅当存在k ∈ [1,n],满足: x 1 ≤ x 2 ≤ ⋯ ≤ x k ≥ x k + 1 ≥ ⋯ ≥ x n x_1 \le x_2 \le \cdots \le x_k \ge x_{k+1} \ge \cdots \ge x_n x1x2xkxk+1xn现在小A可以进行若干次操作,每次可以交换序列中相邻的两个项,现在他想知道最少操作多少次之后能够使序列变为优美的。

Input Format
第一行一个正整数n,表示序列的长度。接下来一行n个整数,表示初始的序列。

Output Format
输出一行一个整数,表示最少需要的操作次数。

Sample Input

5
3 4 5 1 2

Sample Output

1

Constraints
对于30%的数据,n ≤ 12
对于60%的数据,n ≤ 100000, a i a_i ai互不相同
对于100%的数据,n, a i a_i ai≤ 100000

简要思路:这题的主要思想是逆序对。这题的任务是以最少的移动次数构造一个凸函数,可以用树状数组先统计完一边,再统计另一边求两个方向的逆序对。对于每一个数,选逆序对最少的一边,统计进入答案即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#define N 100005
#define ll long long
#define lowbit(x) ((x)&(-(x)))
int n;
ll num[N] , cnt1[N] , cnt2[N];
template < typename T >
inline void read( T & res ) {
	T pd = 1;
	res = 0;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
}
struct Tree{
	ll c[N];
	inline void insert( int x , int y ) {
		for ( register int i = x ; i <= n ; i += lowbit(i) ) {
			c[i] += y;
		}
	}
	inline ll query( int x ) {
		ll ans = 0;
		for ( register int i = x ; i > 0 ; i -= lowbit(i) ) {
			ans += c[i];
		}
		return ans;
	}
}tree1,tree2;
inline ll min( ll a , ll b ) {
	return a < b ? a : b;
}
int main () {
	freopen( "time.in" , "r" , stdin );
	freopen( "time.out" , "w" , stdout );
	read(n);
	for ( int i = 1 ; i <= n ; ++i ) {
		read(num[i]);
	}
	for ( int i = 1 ; i <= n ; ++i ) {
		cnt1[i] = i - 1 - tree1.query(num[i]);
		tree1.insert( num[i] , 1 );
	}
	for ( int i = n ; i >= 1 ; --i ) {
		cnt2[i] = n - i - tree2.query(num[i]);
		tree2.insert( num[i] , 1 );
	}
	ll ans = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		ans += min( cnt1[i] , cnt2[i] );
	}
	printf("%lld",ans);
	return 0;
}

T3:Cover

Description
小A现在想用m条彩灯去装饰家中的走廊,走廊可以视作一个[1, n]的区间,每一条彩灯都能覆盖一个子区间,并且有一个特定的美观程度。然而为了降低装饰的难度,彩灯能够覆盖的区间两两之间只有包含和不相交的关系,同时为了避免光污染,他希望每个[1, n]中的点至多被k条彩灯覆盖。现在小A希望你能告诉他,k = 1, 2, … , m时,选出的彩灯的最大美观程度之和时多少。

Input Format
第一行两个个整数n, m表示区间的长度与彩灯的数量。
接下来m行,每行三个整数 l i l_i li r i r_i ri a i a_i ai表示一条彩灯能够覆盖的区间以及它的美观程度。

Output Format
输出一行m个整数,第i个数表示k = i时的最大美观程度。

Sample Input

25 6
1 2 10
2 3 10
1 3 21
3 4 10
4 5 10
3 5 19

Sample Output

41 80 80 80 80 80

Constraints
对于25%的数据,m ≤ 20
对于45%的数据,n, m ≤ 5000
对于另外25%的数据,所有 a i a_i ai相同
对于100%的数据,1 ≤ l i l_i li r i r_i ri≤ n, m ≤ 300000, a i a_i ai 1 0 9 10^9 109

简要思路:这题其实是一道考验我们手工技巧的模拟题,并没有特别复杂的东东(如果有的大佬想到了差分表请轻喷)。
手工一共分为两步,第一步,根据区间的包含关系建树;第二步,构造子树的前 k k k大的选择方式并合并。
第二步可参见代码注释,第一步快速建树的方法值得讨论一下。
要想每个父区间只会与直接儿子区间连边,先把每个区间按照l升序,r降序来排序,然后可使用单调栈处理,代码如下:

for ( int i = 1 ; i <= m ; ++i ) {
	while ( sta.back() != 0 && num[sta.back()].r < num[i].r ) {
		sta.pop_back();
	}
	g[sta.back()].push_back(i);
	sta.push_back(i);
}

处理效果图如下:
图1
至于第二步合并方法,请参考代码(不要畏惧set和迭代器,它们其实是OIer的好朋友)。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <set>
#define ll long long
#define N 300005
using namespace std;
int n , m;
struct node{
	int l , r , val;
}num[N];
int head[N] , que[N];
vector<int> g[N];
multiset<ll> f[N];
template < typename T >
inline void read( T & res ) {
	T pd = 1;
	res = 0;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
}
inline bool cmp( node aa , node bb ) {
	if ( aa.l != bb.l ) {
		return aa.l < bb.l;
	} else {
		return aa.r > bb.r;
	}
}
inline void merge( multiset<ll> &u , multiset<ll> &v ) {
	if ( u.size() < v.size() ) {
		swap( u , v );
	}
	vector<ll> tem;
	for ( multiset<ll>::iterator i = v.begin() ; i != v.end() ; i++ ) {
		tem.push_back( *i + *u.begin() );
		u.erase( u.begin() );
	}
	for ( int i = 0 ; i < tem.size() ; ++i ) {
		u.insert(tem[i]);
	}
	return;
}
inline void dfs( int cur ) {
	for ( int i = 0 ; i < g[cur].size() ; ++i ) {
		int sn = g[cur][i];
		dfs( sn );
		merge( f[cur] , f[sn] );
	}
	f[cur].insert(-num[cur].val);
}
int main () {
	freopen( "cover.in" , "r" , stdin );
	freopen( "cover.out" , "w" , stdout );
	read(n);
	read(m);
	for ( int i = 1 ; i <= m ; ++i ) {
		read(num[i].l);
		read(num[i].r);
		read(num[i].val);
	}
	sort( num + 1 , num + 1 + m , cmp );
	vector<int> sta;
	sta.push_back(0);
	for ( int i = 1 ; i <= m ; ++i ) {
		while ( sta.back() != 0 && num[sta.back()].r < num[i].r ) {
			sta.pop_back();
		}
		g[sta.back()].push_back(i);
		sta.push_back(i);
	}
	dfs( 0 );
	ll ans = 0;
	for ( int i = 1 ; i <= m ; ++i ) {
		if ( f[0].size() ) {
			ans -= *f[0].begin();
			f[0].erase(f[0].begin());
		}
		printf("%lld ",ans);
	}
	return 0;
}

结语:今天的总结就到这了,明天还要继续努力啊QWQ。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值