文章目录
- [G. Hard Brackets Problem](https://codeforces.com/gym/104768/problem/G)
- [M. Flipping Cards](https://codeforces.com/gym/104768/problem/M)
- [K. Randias Permutation Task](https://codeforces.com/gym/104768/problem/K)
- [B. The Game](https://codeforces.com/gym/104768/problem/B)
- [I. Barkley II](https://codeforces.com/gym/104768/problem/I)
G. Hard Brackets Problem
题意:
Putata 正在用他最喜欢的集成开发环境编码。这个集成开发环境的最大亮点是括号补全功能。该函数的工作原理如下:假设当前屏幕上显示的是 S|T ,其中 | 是光标的当前位置,而 S和 T 是两个(可能为空)字符串。
- 如果 Putata 输入左括号"(",屏幕上将显示 S(|)T ,其中 | 是光标的新位置。
- Putata 输入右括号’)'。如果 T=)T′ ,即 T 以右括号开头,则字符串不会改变,光标将移动到下一个字符的右侧,屏幕上将显示 S)|T′ ,否则屏幕上将显示 S)|T 。
普塔塔昨晚工作得很辛苦,早上醒来时,他只记得保存在电脑上的代码,而且只输入了几个小括号。请帮他找出他按顺序输入的括号序列,或者告诉他只输入括号是不可能输入这个字符串的
主要就是弄懂光标是怎么变化的
括号匹配考虑栈
当在嵌套括号里面时,不需要考虑光标,如果是(),就输入(,如果是),就输入)
当在嵌套括号外面时,就需要先将光标跳出去,就几层就需要输入几个)
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N=1e6+10;
int vis[N];
string s;
void solve() {
cin>>s;
for(int i=0;i<(int)s.size();i++) vis[i]=-1;
stack<int>q;
vector<char>ans;
for(int i=0;i<(int)s.size();i++){
if(s[i]=='(') {
q.push(i);
int x=i-1;
while(s[x]==')'&&vis[x]!=-1){
ans.push_back(')');
x--;
}
ans.push_back('(');
}
else{
if(q.size()){
int t=q.top();
q.pop();
vis[i]=t;
}
else{
int x=i-1;
while(s[x]==')'&&vis[x]!=-1){
ans.push_back(')');
x--;
}
ans.push_back(')');
}
}
}
if(q.size()) cout<<"impossible"<<endl;
else{
for(auto v:ans) cout<<v;
cout<<endl;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
cin>>t;
while(t--) {
solve();
}
return 0;
}
M. Flipping Cards
题意:
n 张牌摆成一排,其中 n 是奇数。每张牌的两面都写着数字。在 i 这张牌上, ai 面朝上, bi 面朝下。
格莱美希望最大化所有朝上数字的中位数。为了达到这个目的,她可以进行以下操作 最多一次。
选择一个区间 [l,r] ,翻转区间内的所有纸牌。翻牌后, bi 朝上, ai 朝下,即为 i∈[l,r] 。
请帮格莱美计算在她的最优策略下所有朝上的数字的中位数。
请回忆一下,一个数列的中位数是数列中最大的第(n+1)/2 个数
看到这道题我就觉得可以秒了
求最大的中位数,想到2022南京的D. Chat Program
求最大的第k大的值,那么check某个数x时,只需要保证第k大的数大于等于x就返回true,因为我们要的是最大的第k大的数,那么第k大的数大于等于x就继续往大了找,最终找到的就是第k大的数刚好等于x,此时x是最大的
对区间进行操作,且有两个变量,想到2021南京的C. Klee in Solitary Confinement
当有两个量时,区间问题,那么用前缀和就会出现sum1[r]-sum1[l-1]和sum2[r]-sum2[l-1],那么sum1[r]和sum2[r]合在一起,sum1[l-1]和sum1[l-1]合在一起,就会大大简化
这题不就是那两题的结合版吗
二分x
sum1[i]表示a中前缀大于等于x的个数,sum2[i]表示b中前缀大于等于x的个数
设a中初始大于等于x的个数为tot
假设操作区间[l,r],那么操作之后a中大于等于x的个数即为tot-(sum1[r]-sum1[l-1])+(sum2[r]-sum2[l-1])
简化一下==> tot+sum2[r]-sum1[r]+sum1[l-1]-sum2[l-1]
其中tot是固定的,sum2[r]-sum1[r]在右端点固定时也是不变的,所以我们只要维护前缀最大的sum1[l-1]-sum2[l-1],一边枚举一边维护即可
本来信心满满,以为能1a,但是一直wa4,后来才发现输入本应是
for (int i = 1; i <= n; i++) cin >> a[i]>>b[i];
我却输成了
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
最搞笑的是样例竟然还都是对的,导致一直找不到问题
所以一定要注意输入的顺序
#include <bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N = 3e5 + 10;
int a[N], b[N];
int sum1[N], sum2[N];
int n;
bool check(int x) {
for (int i = 1; i <= n; i++) {
sum1[i] = sum1[i - 1] + (a[i] >= x);
sum2[i] = sum2[i - 1] + (b[i] >= x);
}
int tot = sum1[n];
int ans = tot;
int pre = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, tot + pre + sum2[i] - sum1[i]);
pre = max(pre, sum1[i] - sum2[i]);
}
if (ans >= (n + 1) / 2) return true;
return false;
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i]>>b[i];
int l = 1, r = 1e9;
while (l < r) {
int mid = l + r +1 >> 1;
if(check(mid)) l=mid;
else r=mid-1;
}
cout<<l<<endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
// cin>>t;
while (t--) {
solve();
}
return 0;
}
K. Randias Permutation Task
题意:
对于长度为 n 的两个排列 A 和 B ,兰迪亚斯可以生成长度为 n 的排列 C 作为 C=A∘B:对于每个 1≤i≤n , C[i]=A[B[i]] .
现在他得到 m 个排列 A1,A2,…,Am ,每个排列的长度为 n 。他想选择一个非空的索引集 i1,i2,…,ik ( 1≤k≤m , 1≤i1<i2⋯<ik≤m),并计算出 C=(((Ai1∘Ai2)∘Ai3)∘Ai4)⋯∘Aik。兰迪亚斯想知道,他能产生多少种可能的排列 C ?请输出答案,模数为 1e9+7
1≤n⋅m≤180
可以发现答案总数很少,为min(n!,2^m),而n*m<=180,所以答案总数最大为362880,所以根本不需要取模
我们直接考虑暴力,每次枚举当前的排列和前面的集合,集合我们用一个set,由于答案总数不超过362880,所以set的大小不超过362880,用set的复杂度为log(362880),然后我们枚举当前排列共m次,每个排列n个,前面集合大小不超过362880,所以总共复杂度为n*m*362880*log(362880)=大概1e9,时限是2s,cf交上去是1264ms,一般的评测机1s可以跑4e8,codeforces好像1s可以跑1e9
一定要注意时限是几秒,不一定总是1秒,不要直接当1秒做了
然后我突然萌生了一个奇妙的想法,我想验证一下是否真的是1e9次,于是我在最前面加了1e9次循环,然后交到cf上,时间仅差80ms,说明确实和1e9是一个量级
于是我又突发奇想,想到时测一下xcpc的评测机,想测xcpc评测机1s能跑多少次,先做一个签到题,然后在这个签到题前面加1e9次循环看能否再次ac,当然是在热身赛上试,可不敢在正式赛冒险
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N=200;
int a[N][N];
int p[N];
int tmp[N];
int n,m;
void solve() {
cin>>n>>m;
vector<vector<int>>a(m+1, vector<int>(n+1));
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
cin>>a[i][j];
}
}
set<vector<int>>s;
for(int i=1;i<=m;i++){
set<vector<int>>t;
for(auto v:s){
vector<int>res(n+1);
for(int j=1;j<=n;j++){
res[j]=v[a[i][j]];
}
t.insert(res);
}
for(auto v:t) s.insert(v);
s.insert(a[i]);
}
cout<<s.size()<<endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
// cin>>t;
while(t--) {
solve();
}
return 0;
}
B. The Game
题意:兰迪亚斯正在玩一个游戏。他得到了两个多集合(可以包含重复元素的集合) A 和 B ,分别由 n 和 m 个整数 A1,A2,…,An ,B1,B2.…,Bm组成。他可以进行任意多次下面的运算:
- 在 x∈A 中选择一个元素 x ,将 xx 改为 x+1 。然后删除 A 中的最小元素,注意如果 A 中有多个最小元素,我们只删除其中一个。
例如,如果 A={4,4,5,5,6} ,我们选择 x=5 ,那么 A 在一次操作后将变成 {4,5,6,6} 。
他想知道,经过几次(可能是零)运算后,他能否做出 A=B ?如果可以,请帮助他确定需要进行哪些运算。
如果对于所有 x , x 在 A 和 B 中出现的次数相同,则认为两个多集合相同。
可以看出来这是一个构造类题,应该是存在一个最优策略去模拟判断是否可行,首先肯定得模拟过程的,不然肯定判断不了,试几个例子就能知道,其次要想模拟的话,必须遵循一个统一的策略,即为最优策略,或者是不劣的
先来看一道类似的模拟构造题,来自2024牛客多校3的D题Dominoes!,大致题意是
给定n张多米诺骨牌,每张骨牌上都写有1和 1 0 9 10^9 109之间的两个正整数。您需要将这些多米诺骨牌排成一条水平线,使每张骨牌的两端都与相邻骨牌的两端相吻合(线的两端除外)。在排列骨牌时,您可以选择左边或右边的数字。我们的任务是确定是否存在相邻骨牌相碰两端的数字总是不同的排列。如果存在这种排列,请提供一个可能的解决方案
最优策略是如果左右两端相同的没有了,那么用左右端不同的一定可以,如果不行只要翻转一下即可;如果左右两端相同的仍然有,那么优先用左右两端相同的且个数最多的(左右两端不同的任何时候都可以用,用来补的);如果左右两端相同的有,但是个数最多的那个不能用,那么看个数第二多的,如果有,那么一定可以用,因为个数第二多的和个数第一多的值一定不一样,如果没有,那么只能用左右两端不同的进行补
可以看到最优策略可能比较复杂,这种题还是比较难的,然后可以用优先队列等stl辅助实现
#include<bits/stdc++.h> #define endl '\n' #define int long long using namespace std; typedef pair<int,int>PII; struct node{ PII p; int cnt; bool operator<(const node&W)const{ return cnt<W.cnt; } }; priority_queue<node>q1,q2; int n; void solve() { cin>>n; map<PII,int>mp1,mp2; for(int i=0;i<n;i++){ int x,y; cin>>x>>y; if(x==y) mp1[{x,y}]++; else mp2[{x,y}]++; } for(auto[p1,c1]:mp1) q1.push({p1,c1}); for(auto[p2,c2]:mp2) q2.push({p2,c2}); int ban=0;//表示上一个的右端为ban,所以当前的左端就不能用ban vector<PII>ans; auto get=[&]()->PII{ //如果左右两端相同的没有了,那么用左右端不同的一定可以,如果不行只要翻转一下即可 if(q1.size()==0){ if(q2.size()==0){ return {ban,ban}; } auto [p2,c2]=q2.top(); q2.pop(); if(c2>1) q2.push({p2,c2-1}); if(p2.first==ban) swap(p2.first,p2.second); return p2; } //如果左右两端相同的仍然有,那么优先用左右两端相同的且个数最多的(左右两端不同的任何时候都可以用,用来补的) auto [p1,c1]=q1.top(); q1.pop(); if(p1.first!=ban){ if(c1>1) q1.push({p1,c1-1}); return p1; } //左右两端相同的有,但是个数最多的那个不能用 //那么看个数第二多的,如果有,那么一定可以用,因为个数第二多的和个数第一多的值一定不一样 //如果没有,那么只能用左右两端不同的进行补 if(q1.size()==0){ q1.push({p1,c1}); if(q2.size()==0){ return {ban,ban}; } auto [p2,c2]=q2.top(); q2.pop(); if(c2>1) q2.push({p2,c2-1}); if(p2.first==ban) swap(p2.first,p2.second); return p2; } auto [p_1,c_1]=q1.top(); q1.pop(); q1.push({p1,c1}); if(c_1>1) q1.push({p_1,c_1-1}); return p_1; }; for (int i = 0; i < n; i++) { auto [x, y] = get(); if(x==ban){ cout<<"No"<<endl; return; } ans.push_back({x,y}); ban = y; } cout<<"Yes"<<endl; for(int i=0;i<(int)ans.size();i++) cout<<ans[i].first<<' '<<ans[i].second<<endl; } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t=1; // cin>>t; while(t--) { solve(); } return 0; }
这题的话其实和上面那一题可以说是一个类型
首先想一个最优策略:
让a中最大的m个数去匹配b中的m个数,需要通过+1操作,所以计算出需要+1的次数,记为cnt,具体a和b先升序,a[n-m+i]去匹配b[i],一一对应,但若存在a[n-m+i]>b[i],肯定无解,否则cnt= ∑ i = 1 m \sum_{i=1}^{m} ∑i=1m b[i]-a[n-m+i]我们一共需要加cnt次1,同时会执行删除最小的数cnt次,我们需要最终m个最大的数所差的次数cnt等于剩余其它数的个数,一定是操作最小值最优,由此用到优先队列或者multiset
具体,利用两个multiset,s2维护m个最大的数,s1维护剩下的数,然后每次操作最小的数也就是s1的头,维护cnt,直到s1的个数为cnt个,再检查一遍最大的m个数是否存在超过b[i]的情况,如果有就无解,否则就直接操作最大的m个数就可以了
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N=3e5+10;
int a[N],b[N];
int c[N];
int n,m;
void solve() {
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++) cin>>b[i];
sort(a+1,a+1+n);
sort(b+1,b+1+m);
int cnt=0;
for(int i=1;i<=m;i++){
if(a[n-m+i]>b[i]){
cout<<-1<<endl;
return;
}
cnt+=b[i]-a[n-m+i];
}
multiset<int>s1,s2;
for(int i=1;i<=n-m;i++) s1.insert(a[i]);
for(int i=n-m+1;i<=n;i++) s2.insert(a[i]);
vector<int>option;
if((int)s1.size()<cnt){
cout<<-1<<endl;
return;
}
while((int)s1.size()>cnt){
auto t=s1.begin();
option.push_back(*t);
int x=*t+1;
s1.erase(t);
if(x>*s2.begin()){
auto t2=s2.begin();
s1.insert(*t2);
cnt-=x-*t2;
s2.erase(t2);
s2.insert(x);
}
else s1.insert(x);
s1.erase(s1.begin());
if(cnt<0){
cout<<-1<<endl;
return;
}
}
for(int i=1;i<=m;i++){
c[i]=*s2.begin();
s2.erase(s2.begin());
}
for(int i=1;i<=m;i++){
if(c[i]>b[i]){
cout<<-1<<endl;
return;
}
int d=b[i]-c[i];
for(int j=0;j<d;j++) {
option.push_back(c[i]+j);
}
}
cout<<option.size()<<endl;
for(auto v:option) cout<<v<<' ';
cout<<endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
cin>>t;
while(t--) {
solve();
}
return 0;
}
I. Barkley II
题意:
许教授是猪场大学编程队的教练。他的团队中有 n 名学生。所有算法都被许教授按照难度从高到低的顺序编号,从 1 到 m 。也就是说, 1 是最简单的算法,而 m 是最难的算法。 i -th的学生掌握了 ai -th最简单的算法。
现在许教授想选择一个满足以下条件的团队:
- 团队中学生的指数构成一个区间。也就是说,存在两个整数 l,r,使得 1≤l≤r≤n 和学生 x 在团队中,当且仅当 l≤x≤r 。
- 团队的评分最大化。团队掌握的算法越多,他们的实力就越强,但如果他们不能在一次竞赛中解决一个难题,他们就会感到更加失望。因此,团队的评分就是团队中学生掌握的种不同算法的数量减去团队中无人掌握的种最简单算法的指数。如果团队中的学生掌握了所有算法,则认为团队中没有学生掌握的最简单算法的指数为 m+1 。例如,如果 m=5 和团队中的 6 名学生分别掌握了算法 2,5,4,4,1,1,则团队的评分为 4−3=1 。
请帮助许教授求出一个团队的最大等级
先来看一个简易版的,洛谷上的一个模板题P1972 [SDOI2009] HH的项链
题意:
HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。
有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题
这样考虑,考虑r固定的情况下,项链中出现的同一个数字,一定是只关心出现在最右边的那一个的
比如项链是:1 3 4 5 1
那么,对于r=5的所有的询问来说,第一个位置上的1完全没有意义,因为r已经在第五个1的右边,对于任何查询的[L,5]区间来说,如果第一个1被算了,那么他完全可以用第五个1来替代。
因此,我们可以对所有查询的区间按照r来排序,然后再来维护一个树状数组,这个树状数组是用来干什么的呢?看下面的例子:
1 2 1 3
对于第一个1,insert(1,1);表示第一个位置出现了一个不一样的数字,此时树状数组所表示的每个位置上的数字(不是它本身的值而是它对应的每个位置上的数字)是:1 0 0 0
对于第二个2,insert(2,1);此时树状数组表示的每个数字是1 1 0 0
对于第三个1,因为之前出现过1了,因此首先把那个1所在的位置删掉insert(1,-1),然后在把它加进来insert(3,1)。此时每个数字是0 1 1 0
如果此时有一个询问[2,3],那么直接求sum(3)-sum(2-1)=2就是答案综上,我们对于右端点相同的统一处理,所有询问区间按照右端点从小到大排序,然后用树状数组处理前缀i,就可以直接回答右端点为i的区间了,一边处理一边回答
由此我们可以快速求出我们想要求的区间的不同算法的数量,但是区间数量太多了,肯定不能枚举所有的区间,我们看能否挑选一些区间,可以根据答案最大等级来找区间,其实就是mex,只不过是从1开始的,我们枚举所有可能的mex值,不一定是真的mex值,我们只要保证区间中不存在mex这个值就行,因为我们枚举了所有可能的mex值,包含了各种情况,不需要一定是真的mex值,对于某个mex,我们找到所有不包含它的极大区间,那么其实就相当于上题的区间询问了
如何找极大区间,我们标记每个值的所有位置,比如3的位置分别为5,10,20,那么不包含3的极大区间有[1,4],[6,9],[11,19],[21,n]
当然我们枚举mex的时候不能枚举[1,m+1],因为m的总和是不限制的,而n的总和是有限制的,我们枚举a去重后的所有值,然后我们来算一下询问区间一共有多少,如枚举mex=1,有3个位置,那么区间个数就是3+1=4,然后由于每个数所在的位置是不同的,所以我们实际上枚举的所有位置都是不重复的,故询问区间个数是O(n)的
但是发现如果全是一个数的话,那么会没有考虑到,以及如果1到max(a[i])均有的话,那么mex为max+1也没考虑到,只要单独考虑一下整个数组即可
注意,多组样例,记得清空数组,特别是树状数组的题
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N=5e5+10;
int a[N];
int tr[N];
int vis[N];
int n,m;
struct node{
int l,r,mex;
bool operator<(const node &W)const{
return r<W.r;
}
};
int lowbit(int x)
{
return x & -x;
}
int sum(int x){
int res=0;
while(x) res+=tr[x],x-=lowbit(x);
return res;
}
void add(int x,int c){
while(x<=n) tr[x]+=c,x+=lowbit(x);
}
void solve() {
cin>>n>>m;
map<int,vector<int>>pos;
for(int i=1;i<=n;i++) {
cin>>a[i];
pos[a[i]].push_back(i);
}
vector<node>res;
for(auto [x,s]:pos){
int len=s.size();
for(int i=1;i<len;i++){
if(s[i-1]+1<=s[i]-1) res.push_back({s[i-1]+1,s[i]-1,x});
}
if(s[0]>1) res.push_back({1,s[0]-1,x});
if(s[len-1]<n) res.push_back({s[len-1]+1,n,x});
}
sort(res.begin(),res.end());
//单独考虑整个区间
set<int>q;
int mex=1;
for(int i=1;i<=n;i++){
q.insert(a[i]);
while(q.count(mex)) mex++;
}
int ans=q.size()-mex;
int idx=0;
for(auto [l,r,mex]:res){
while(idx<r){
idx++;
if(vis[a[idx]]){
add(vis[a[idx]],-1);
}
add(idx,1);
vis[a[idx]]=idx;
}
ans=max(ans,sum(r)-sum(l-1)-mex);
}
for(int i=1;i<=n;i++) vis[a[i]]=0;
for(int i=1;i<=n;i++) tr[i]=0;
cout<<ans<<endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
cin>>t;
while(t--) {
solve();
}
return 0;
}