Codeforces Round 933 (Div. 3)题解(ABCDEF)

题目链接 - Codeforces Round 933 (Div. 3)

博客中的代码仅供参考,请勿直接提交源代码,这对提升实力毫无作用!!!

题目写一半,自己不小心把编译器搞坏了,中途又重新下载mingw呜呜呜,好在这场时间长

A. Rudolf and the Ticket

题意

有两个数组a和b,长度分别为n,m。即 a 1 , a 2 , . . . , a n , b 1 , b 2 , . . , b m a_1,a_2,...,a_n,b_1,b_2,..,b_m a1,a2,...,an,b1,b2,..,bm

现要求你从两个数组中各选择一个数 a i , b j a_i,b_j ai,bj 并且要求 a i + b j ≤ k a_i + b_j \le k ai+bjk ,问有多少种选法。

思路

二分查找, 对b数组排序,然后枚举 a i a_i ai , 使用upperbound来在b中查找第一个大于 k − a i k - a_i kai 的下标p,那么b数组中的区间 [ 0 , p − 1 ] [0,p-1] [0,p1]上的数都满足和 a i a_i ai 相加之后小于等于 k k k。 于是产生了 p p p的贡献。按此理计算出总贡献即可。复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

代码

#include<bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define rep(i,l,r) for(int i = l;i<=r;i++)
#define per(i,r,l) for(int i = r;i>=l;i--)
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int,int> PII;
void solve(){
    int n,m,k;
    cin>>n>>m>>k;
    vector<int> a,b;
    rep(i,1,n){
        int num;cin>>num; a.push_back(num);
    }
    rep(i,1,m){
        int num;cin>>num; b.push_back(num);
    }
    int sum = 0;
    sort(b.begin(),b.end());
    for(auto p : a){
        int t = k - p;
        sum += upper_bound(b.begin(),b.end(),t) - b.begin();
    }
    cout<<sum<<endl;
}
signed main(){
    int T = 1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

B - Rudolf and 121

题意

现在有一个长度为 n n n的数组 a a a a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an 要求你使用任意次如下操作,问能否使得数组a全为0 。

选择一个下标 i ( 2 ≤ i ≤ n − 1 ) i(2 \le i \le n-1) i(2in1),然后进行下面三步:

  1. a i − 1 − = 1 a_{i-1} -= 1 ai1=1
  2. a i − = 2 a_{i} -= 2 ai=2
  3. a i + 1 − = 1 a_{i+1} -= 1 ai+1=1

思路

贪心,观察操作可以发现,我们若想让 a 1 a_1 a1 归零, 那么就必须选择 i = 2 i = 2 i=2 进行操作,并且要准确地操作 a 1 a_1 a1 次。 此时 a 1 a_1 a1就一定变为了0 。更新数组,即更新 a 1 , a 2 , a 3 a_1,a_2,a_3 a1,a2,a3

然后我们就按照此法再令 i = 3 i = 3 i=3 ,进行 a 2 a_2 a2次操作,来试图将 a 2 a_2 a2 归零。 但我们发现如果某个数变成了负数,那么他永远无法变成0,因此我们就可以输出NO并结束这组样例。

如此法遍历整个数组即可。

在遍历完整个数组后,我们还无法得知 a n − 1 , a n a_{n-1}, a_n an1,an 是否成功清零,判断一下即可。

代码

#include<bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define rep(i,l,r) for(int i = l;i<=r;i++)
#define per(i,r,l) for(int i = r;i>=l;i--)
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int,int> PII;
void solve(){
    int n;
    cin>>n;
    vector<int> vec(n+5);
    rep(i,1,n) cin>>vec[i];
    rep(i,2,n-1){
        if(vec[i-1]<0||vec[i]<0||vec[i+1]<0){cout<<"No\n";return ;}//如果某个数变成了负数
        if(vec[i-1] == 0) continue;
        int t = vec[i-1];//要进行的操作次数
        vec[i] -= 2 * t;
        vec[i+1] -= t;
        vec[i-1] -= t;
    }
    if(vec[n-1] != 0 || vec[n]!=0){cout<<"No\n";return ;}//如果这两个数没有成功清零
    cout<<"Yes\n";
}
signed main(){
    int T = 1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

C - Rudolf and the Ugly String

题意

如果一个字符串中存在子串map 或者 pie ,那么就说这个子串是丑陋的,否则就说它是美丽的。现在给你一个字符串,问最少需要删除几个字符才能使得字符串是美丽的。

思路

我们需要拆散这个字符串中的mappie , 容易发现,如果存在某个子串是map ,那么我们删除中间的a即可,这样能够删除这里的map,并且保证不会出现新的map ,pie也是同理, 因此我们找这个字符串中存在几个map或者pie子串即可(吗?)

如果仅仅想到这一步,那么你将获得一发罚时。因为如果字符串中存在子串mapie ,那么我们照此法将会删除a和i两个字符,然而直接删除p字符就可以同时拆散mappie

所以最终答案应为字符串中map与pie的数量相加再减去mapie的数量

代码

#include<bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define rep(i,l,r) for(int i = l;i<=r;i++)
#define per(i,r,l) for(int i = r;i>=l;i--)
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int,int> PII;
void solve(){
    int n;
    cin>>n;
    string str;
    cin>>str;
    int ans = 0;
    for(int i = 0;i<n-2;i++){
        if(i<=n-5 && str.substr(i,5) == "mapie"){ans--;} //每出现一个mapie就让答案减一
        if(str.substr(i,3) == "map") ans++;
        if(str.substr(i,3) == "pie") ans++;
    }
    cout<<ans<<endl;
}
signed main(){
    int T = 1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

D. Rudolf and the Ball Game

题意

有n个人按顺时针顺序s围成一个环,标号分别为 1 , 2 , . . , n 1,2,..,n 1,2,..,n , 如图:

在这里插入图片描述

起初球在第x个人的手中,下面进行m次传球。每次传球遵循如下格式:r c 其中r为一个整数,表示传球的距离。c为0 1 ? 中的一个字符 , 0表示下一步要顺时针传球, 1表示这次传球是逆时针的,?则表示这次传球不确定是顺时针还是逆时针。如上图中2号人顺时针传距离5时,传给了7,逆时针时则传给了4 。

问进行完m次传球后, 哪些人可能拿到球。

思路

dp,我们使用一个数组 d p dp dp记录上一步都哪些人可能获得球后,起初 d p [ x ] = 1 dp[x] = 1 dp[x]=1 ,其他值为0 。

对于每一步传球操作,根据操作情况来生成一个新的数组 f f f表示进行完这步操作后哪些人可能会获得球。然后将 f f f内容复制给 d p dp dp之后, 清空 f f f 。不断重复如此即可得到最终的数组。

对标号 1 , 2 , . . . , n 1,2,...,n 1,2,...,n 的人进行循环取模操作会比较复杂,可以考虑令所有人的标号减一,最终输出的时候通过加一来恢复原始标号。

代码

还有一种存储方式是用set, 但原理相同,这里不在赘述

#include<bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define rep(i,l,r) for(int i = l;i<=r;i++)
#define per(i,r,l) for(int i = r;i>=l;i--)
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int,int> PII;
void solve(){
    int n,m,x;
    cin>>n>>m>>x;
    //这里的vector就等价于数组,但是支持直接赋值整个数组
    vector<bool> dp(n+5);//老数组
    vector<bool> f(n+5);//新数组
    dp[x-1] = 1; //做了减一操作,更方便取模
    rep(i,1,m){
        int k;char op;
        cin>>k>>op;
        for(int i =0;i<n;i++){
            if(dp[i]){
                if(op !='0') f[(i-k+n)%n] = 1; //1和?会进行逆时针操作
                if(op !='1') f[(i+k)%n] = 1;//0和?会进行顺时针操作
                //算是一个小技巧,可以少写一个判断
            }
        }
        dp = f;//令dp等于f
        fill(f.begin(),f.end(),0);//清空f
    }
    vector<int> ans;//记录哪些人可能获得球
    rep(i,0,n-1) if(dp[i]) ans.push_back(i+1);
    cout<<ans.size()<<'\n';
    for(auto p : ans){
        cout<<p<<" ";
    } 
    puts("");

}
signed main(){
    int T = 1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

E. Rudolf and k Bridges

题意

现在有n行m列的网格a, a i , j a_{i,j} ai,j 表示第 i i i行第 j j j 列的海水深度 , 保证第1列和第m列是大陆, 深度为0。

在这里插入图片描述

对每行你可以按照如下规则建桥:

在第 1 1 1列和第 m m m列必须要有支柱。然后你可能还需要在海中树立一些柱子。需要保证不存在任何两个相邻的柱子之间的距离大于d,如果两个柱子的距离分别为 i , j i,j i,j 那么他们之间的距离是 j − i − 1 j - i - 1 ji1

建造一个桥所消耗的费用为 这座桥上所有柱子所在的海水深度加一 的和。

现在需要你建造k个连续的桥。即选择一个 i , i ≤ n − k + 1 i,i\le n-k+1 i,ink+1,在 i , i + 1 , . . . , i + k − 1 i,i+1,...,i+k-1 i,i+1,...,i+k1 行都建一个桥。问最少需要多少费用。

思路

比较容易得知,对于建造不同的桥,他们的费用是互不影响的。因此我们可以分别计算这n座桥的建造费用,然后选择区间和最小的那组即可。

我们先讲在找到每行的建桥费用并存入ans数组后, 如何找这k个连续的桥(如果你已经会了就跳过)

我们对ans数组进行计算前缀和 ,存入sum中, 于是 s u m [ i ] − s u m [ i − k ] sum[i] - sum[i-k] sum[i]sum[ik] 则表示了从第 i − k + 1 i-k+1 ik+1 开始建k座桥需要的总花费。于是枚举每一个可能的i,令 r e s res res为所有可能中的最小值即可。


下面来探讨如何计算建造某个桥的费用。

我们可以使用dp的思想, d p [ i ] dp[i] dp[i] 表示在第 i i i列立下一根柱子的情况下,建好前 i i i格最少需要多少花费,这样 d p [ m ] dp[m] dp[m] 就是建造这整座桥的花费。我们 d p [ i ] dp[i] dp[i] 必须由 d p [ i − 1 ] , d p [ i − 2 ] , . . . , d p [ i − k − 1 ] dp[i-1],dp[i-2],...,dp[i-k-1] dp[i1],dp[i2],...,dp[ik1] 中的某一个转移而来,可以发现,在建造完成第 i i i格的柱子后,不论之前的方案如何,对之后的影响都是相同的。于是我们选择这些元素中值最小的作为母状态即可。

但是如果我们直接枚举 d p [ i − 1 ] , d p [ i − 2 ] , . . . , d p [ i − k − 1 ] dp[i-1],dp[i-2],...,dp[i-k-1] dp[i1],dp[i2],...,dp[ik1] 这些内容,那么计算一个桥的复杂度就是 O ( m ∗ d ) O(m*d) O(md) 这显然会导致超时。于是我们考虑使用数据结构来加速。

下面介绍方案一:优先队列

我们使用优先队列存储一个结构体,表示柱子的下标 x x x以及 d p [ x ] dp[x] dp[x] , 并且规定队头元素是 d p [ x ] dp[x] dp[x]最小的那个,这样我们就保证可以在O(1)的时间内选出值最小的那个元素。 同时我们每次使用队头元素前都要检查队头元素是否过期(若队头元素的下标 x x x与当前下标 i i i 的距离超过了d ,那么它就已经过期,在之后都无法使用),如果过期,就直接出队。

通过上面的构造,我们的优先队列就满足了如下两个性质:

  1. 队头元素一定是花费最小的那个。
  2. 队头元素一定没有过期

于是我们就可以借此来知晓 d p [ i ] dp[i] dp[i]的值,即 d p [ i ] = p q . f r o n t ( ) . w a s + d e p + 1 dp[i] = pq.front().was + dep+1 dp[i]=pq.front().was+dep+1 。 其中 w a s was was表示花费, d e p dep dep表示第 i i i格的深度。之后将 ( i , d p [ i ] ) (i,dp[i]) (i,dp[i])入队。 复杂度为 O ( m l o g m ) O(mlogm) O(mlogm)

方案二: 单调队列

很常用的一个数据结构。我们使用一个双端队列,并维护这个队列是递增的。和上面的优先队列原理类似。我们保证队头元素一定不是过期的,如果已经过期就出队它。(若下标 x x x i i i的距离大于 d d d,那么我们说 x x x已经过期了),同时在入队时,我们保证入队的这个元素一定比队尾元素更大,如果违背了这个规则,我们就不断从队尾出队(这是一个双端队列,所以可以从队尾出队),等到不违背这个规则时,再入队元素。

于是我们的队列就满足了如下两个性质:

  1. 队列中的所有元素从队头到队尾单调递增,因此队头元素一定最小
  2. 队列中队头元素一定不是过期的。

于是我们就可以保证得到正确的 d p [ i ] dp[i] dp[i] ,于是就可以得到一个桥的花费,即 d p [ m ] dp[m] dp[m]

代码

优先队列:

#include<bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define rep(i,l,r) for(int i = l;i<=r;i++)
#define per(i,r,l) for(int i = r;i>=l;i--)
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int,int> PII;
struct Node
{
    int x; //下标x
    int wast;//花费waste
    friend bool operator<(Node n1,Node n2){ //用来决定优先队列的排序顺序
        return n1.wast > n2.wast;
    }
};

void solve(){
    int n,m,k,d;
    cin>>n>>m>>k>>d;
    vector<int> ans;
    ans.push_back(0);
    int t = n;//n还有用
    while(t--){//计算这n行各自的花费
        priority_queue<Node> pq;
        int x;cin>>x;pq.push({1,1});//第一个一定是0,所以直接入队{1,1}
        for(int i = 2;i<=m;i++){
            cin>>x;
            while(pq.top().x+d+1<i){//不断出队过期元素
                pq.pop();
            }
            if(i == m){//如果到达了河对岸
                ans.push_back(pq.top().wast+x+1);//可以存答案了
                break;
            }
            pq.push({i,pq.top().wast+x+1});//入队元素
        }
    }
    for(int i = 1;i<=n;i++){
        ans[i] += ans[i-1];
    }
    int res = INF;
    for(int i =k;i<=n;i++){
        res = min(res,ans[i] - ans[i-k]);
    }
    cout<<res<<endl;
}
signed main(){
    int T = 1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

单调队列(代码来自队友):

这是队友CSDN主页,超强!

#include <iostream>
#include <cstdio>
#include <vector>
#include <set>
#include <queue>
#define mk make_pair
using namespace std;
typedef long long ll;
const int maxn=105;
const int maxm=2e5+5;
const ll inf=1e18;

int T,n,m,k,d;
int a[maxm];
ll dp[maxm];

ll cost[maxn];

int main(){
	cin>>T;
	while(T--){
		cin>>n>>m>>k>>d;
		for(int bridge=1;bridge<=n;bridge++){
			for(int i=1;i<=m;i++)cin>>a[i],dp[i]=inf; //初始化dp为全INF(最大值)
			deque<pair<ll,ll> > q;
			dp[1]=1;
			q.push_back(mk(1,1));
			for(int i=2;i<=m;i++){
				dp[i]=min(dp[i],q.front().first+a[i]+1);
				while(!q.empty() && dp[i]<=q.back().first)q.pop_back();//保证队列单调性
				q.push_back(mk(dp[i],i));
				if(i-d>q.front().second)q.pop_front();//保证队头不过期
			}
//			cout<<dp[m]<<endl;
			
			cost[bridge]=cost[bridge-1]+dp[m];//dp[m]是建这座桥的最少费用,cost直接计算了前缀和
		}
		ll ans=inf;
		for(int i=k;i<=n;i++)
			ans=min(ans,cost[i]-cost[i-k]);
		cout<<ans<<endl;
	}
	return 0;
}

F - Rudolf and Imbalance

题意

现在给你三个数组 a , d , f a,d,f a,d,f , 长度分别为 n , m , k n,m,k nmk,你可以从 d d d f f f中各选出一个数 d i , f j d_i, f_j di,fj 然后把 d i + f j d_i +f_j di+fj 放入a中, 问进行最多一次 该操作后, a数组中的 M A X i = 1 n ′ − 1 ( a i + 1 − a i ) MAX_{i=1}^{n'-1}(a_{i+1} - a_i) MAXi=1n1(ai+1ai) 最小是多少。 其中的 n ′ n' n 表示为进行至多一次操作之后的a的元素数量。

保证a数组是有序的。

思路

由于我们只能插入一个数,因此只有插入到最大的那个间隙中,才能使得最大间隔变短。 因此我们可以先求出最大间隔 m x mx mx和次大间隔 s e m x semx semx , 并找到产生最大间隔的那两个数 l l l r r r,即 r − l = m x r - l = mx rl=mx 。 假设我们找到了一个数 t t t,来插入到 l l l r r r中间,那么整个数组a的最大间隔就 变为了 M A X ( s e m x , a b s ( t − l ) , a b s ( r − t ) ) MAX(semx,abs(t-l),abs(r-t)) MAX(semx,abs(tl),abs(rt)) , 我们设 m i d = l + r 2 mid = \frac{l+r}{2} mid=2l+r ,那么很明显,semx是一个定值,因此我们的t越靠近mid,便越可能得到更小的答案。于是整道题的目标就变为了找到最接近$mid 的那个 的那个 的那个t$ 。

我们对数组 f f f进行排序, 然后枚举 d d d中 的元素 x x x, 使用lower_bound来从 f f f中找到最接近 m i d − x mid - x midx 的两个值作为 t t t的 待选项,借此更新更优的 t t t ,得到更优的 a n s ans ans 。最终我们再和次大间隔 s e m x semx semx m a x max max即可。

需要注意的是,

  1. 注意lower _bound之后可能找到的是数组边界,此时可能会不存在其中一个待选项 t t t
  2. 要特殊考虑n == 2 时没有次大值 s e m x semx semx的情况。

代码

#include<bits/stdc++.h>
using namespace std;
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define rep(i,l,r) for(int i = l;i<=r;i++)
#define per(i,r,l) for(int i = r;i>=l;i--)
const int INF = 0x3f3f3f3f3f3f3f3f;
typedef pair<int,int> PII;
void solve(){
    int n ,m,k;
    cin>>n>>m>>k;
    vector<int> a(n+5);//数组a
    for(int i = 1;i<=n;i++) cin>>a[i];
    int mx = 0,semx = 0;//a中相邻两个数的差,最大和次大
    int l,r;//最大差的左项和右项
    //初始化mx和semx,l,r
    if(n>=3){
        if(a[3]-a[2]>a[2]-a[1]){
            l = a[2],r = a[3];
            mx = a[3]-a[2];
            semx =a[2]-a[1];
        }else {
            l = a[1],r = a[2];
            mx = a[2]-a[1];
            semx =a[3]-a[2];
        }
    }else if(n == 2){
        mx = a[2]-a[1];l = a[1];r = a[2];
    }
    //找mx和semx,l,r
    rep(i,3,n-1){
        if(a[i+1]-a[i]>mx){
            semx = r - l;
            mx = a[i+1]-a[i];
            r = a[i+1],l = a[i];
        }else if(a[i+1]-a[i]>semx){
            semx = a[i+1]-a[i];
        }
    }
    int mid = (l+r)/2;
    vector<int> d,f;
    rep(i,1,m){int num;cin>>num;d.push_back(num);}
    rep(i,1,k){int num;cin>>num;f.push_back(num);}
    sort(f.begin(),f.end());
    int ans = INF;
    for(auto x : d){//枚举d
        int p = upper_bound(f.begin(),f.end(),mid-x) - f.begin();
        if(p!=k){
            int t = f[p]+x;
            int newans = max(abs(l-t),abs(t-r));
            ans = min(ans,newans);
        }if(p!=0){
            int t = f[p-1]+x;
            int newans = max(abs(l-t),abs(t-r));
            ans = min(ans,newans);
        }
    }
    if(ans > mx){cout<<mx<<endl;return;}
    cout<<max(semx,ans)<<endl;
}
signed main(){
    int T = 1;
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值