AcWing第54场周赛

题目列表

A AcWing 4428. 字符串

题目描述

给定一个长度为 n 的由大小写英文字母构成的字符串。

请你判断,该字符串是否包含了全部 26 个英文字母?

注意,无论某个英文字母是以大写还是小写形式出现在字符串中,均视为该字符串包含此字母。

输入格式
第一行包含整数 n。

第二行包含一个长度为 n 的由大小写英文字母构成的字符串。

输出格式
如果给定字符串包含了全部 26 个英文字母,则输出 YES,否则输出 NO。

数据范围
所有测试点满足 1≤n≤100。

输入样例1:
12
toosmallword
输出样例1:
NO
输入样例2:
35
TheQuickBrownFoxJumpsOverTheLazyDog
输出样例2:
YES

分析

可以定义个hash表,遍历字符串的同时记录下字符有没有出现过,最后遍历下hash表,看看26个字母是否都出现了即可。注意大小写字母在本题中等价,遇见大写字母可以手动转换成小写字母,也可以使用tolower函数进行转换。这里我对小写字母一律减去’a’,大写字母一律减去’A’,一个字母的大小写会存储到相同的位置,就不需要进行转换了。

代码

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
bool st[26];
int main(){
    int n;
    string s;
    cin>>n>>s;
    for(int i = 0;i < n;i++) {
        if(s[i] >= 'a' && s[i] <= 'z')  st[s[i] - 'a'] = true;
        else    st[s[i] - 'A'] = true;
    }
    int i;
    for(i = 0;i < 26;i++){
        if(!st[i]){
            cout<<"NO"<<endl;
            break;
        }
    }
    if(i == 26) cout<<"YES"<<endl;
    return 0;
}

B AcWing 4429. 无线网络

题目描述

农夫约翰的农场可以看作一个二维平面。

农场中散布着 n 头奶牛,每头奶牛的位置坐标已知。

农场中还建有 2 个 wifi 基站,每个基站的位置坐标已知。

这 n+2 个位置坐标两两不同。

第一个基站的有效覆盖范围 r1 和第二个基站的有效覆盖范围 r2 均可由约翰自由设定。

因为奶牛喜欢保持电子邮件联系,所以约翰希望所有奶牛都能被无线网络覆盖。

如果一头奶牛满足以下两个条件中的至少一个:

它到第一个基站的距离不超过 r1
它到第二个基站的距离不超过 r2
那么就视为它已被无线网络覆盖。

同时为了降低成本,约翰希望 r12+r22 尽可能小。

请你计算 r12+r22 的最小可能值。

输入格式
第一行包含 5 个整数 n,x1,y1,x2,y2,其中 n 为奶牛数量,(x1,y1) 为第一个基站的坐标,(x2,y2) 为第二个基站的坐标。

接下来 n 行,每行包含两个整数 xi,yi,表示一头奶牛的位置坐标 (xi,yi)。

输出格式
输出 r12+r22 的最小可能值,答案四舍五入到个位。

数据范围
前 4 个测试点满足 1≤n≤10。
所有测试点满足 1≤n≤2000,−107≤xi,yi≤107

输入样例1:
2 -1 0 5 3
0 2
5 2
输出样例1:
6
输入样例2:
4 0 0 5 0
9 4
8 3
-1 0
1 4
输出样例2:
33

分析

这次周赛的第二道题难倒了不少人,一些人第三道都做出来了却没有完成第二道。如果题目考察某种具体的算法,分析出来要用什么算法解决后就容易了,恰恰是一些只需要模拟或者枚举的题目,很难想到解法。

先说下我的解题过程(如果不需要看前面不正确的分析思路可以直接阅读三段之后的正确思路),题目要求r12+r22 最小,首先想到基本不等式,r1 = r2时可以取最小值,但是本题明显用不上,然后想到一个表达式的值最小是不是最优化问题,最容易想到的还是贪心做法,做的题目越多越容易陷入思维惯性里,有一定算法基础的人看见题目很多会直接跳过最暴力的解法,直接思考最优解法。有不少雷达映射范围的题目都是排下序直接贪心解决了,本题我的第一反应也是贪心。
每头奶牛都需要被两个基站中的至少一个的信号覆盖到,那么对于任意一头奶牛,选择哪个基站来接收信号就决定了最终两个基站的信号范围r1和r2的取值。我最先想到的贪心思路是这样的:遍历所有的奶牛,如果某头奶牛已经距离第一个基站距离不超过r1或者距离第二个基站的距离不超过r2,说明可以被网络覆盖到,也就不需要增加r1或者r2的值了。如果奶牛不能被网络覆盖到,就考虑增加其中一个基站的覆盖范围,增加哪个基站的覆盖范围呢?设该头奶牛离第一个基站的距离是l1,离第二个基站的距离是l2。如果增大r1到l1,那么新的解就是l12 + r22,解增大了l12 - r12;如果增大r2到l2,新的解就是r12 + l22,解增大了l22 - r22。这里我没有直接选择增大离奶牛最近基站的覆盖范围,而是选择了对解增幅最小的方案,也就是如果l12 - r12 < l22 - r22,说明增加第一个基站的覆盖范围对解的增幅小,就增加r1到l1,否则增加r2到l2。这种贪心思路是在两个基站原本覆盖范围确定了,出现了新的奶牛,调整基站覆盖范围方案中最优的调整方案。

提交通过了几个用例,检查下代码发现部分中间变量使用了int存储,而距离平方可以达到1014级别,遂调整至long long存储,提交后又多通过了几个用例,当然依旧没有ac本题,因为这题用贪心的思路是找不到最优解的。由于周赛是看不到没有通过的用例是什么,所以我就写了会第三题再回过头来看这题。虽然这种贪心思路可以在动态调整覆盖范围时对解的增幅最小,但是我没法证明它能够使得r12 + r22取得最小值。在没有错误用例的情况下,我只能像写对拍程序那样自己去随机生成用例来找到贪心思想的漏洞了,构造了一个较大距离的点,本意是想看看会不会发生溢出,找到代码漏洞,结果没有溢出,但是却让我发现了贪心思想的不足,比如我构造了一个(-10000000,10000000)坐标的奶牛位置,如果一个基站在原点,我就会得到一个很大的r1,而在遍历奶牛时我动态的求出了r2,就算此时r1足够大,足以覆盖到所有奶牛时,r2取值依旧不是0,说明这个方案不是最优的。举个例子,如果刚开始遍历时r1 = 1,出现一个离第一个基站较远,但是离第二个基站较近的点,我们更新r2 = 2,后面出现了一个超级远的点,更新了r1 = 10000,虽然第二个点刚出现时候,第一个基站不能覆盖到,所以增加了第二个基站的覆盖范围,但是后面第一个基站的覆盖范围增加了,未必不能覆盖到第二个点,这时候之前增加r2的值就没有必要了。贪心思想的不可取在于动态的调整基站覆盖范围,而所有奶牛的位置一开始就是已知的,我们不需要动态调整。

不妨考虑暴力的解决本题,求r12 + r22,我们首先要确定的是r1和r2的取值范围。如果将r1和r2都设置为离它们最远的点的距离,那么网络必然可以覆盖到所有的奶牛,虽然r1和r2的取值看起来是连续的,也就是取0到离它们最远奶牛的距离之间所有的距离,但其实是离散的,也就是说,r1的取值只会在0,到第1个点的距离,到第2个点的距离,…,到第n个点的距离这n + 1个数中取,r2同样的也只有n + 1个可能的取值。既然取值数量有限,我们不妨挨个枚举r1和r2,求出其中的r12 + r22。首先存下两个基站到所有奶牛的距离,然后二重循环来枚举r1和r2的取值,算法的时间复杂度是平方级别的,数据范围是2000,足以ac本题。暴力枚举解决本题,思路很简单,代码也很简洁,大部分人没有及时做出这题的原因我觉得还是思维惯性导致的。

本题是以前的一道csp真题,不过缩小了数据范围,原题的数据范围是10w,显然平方级别的算法没法通过。还有个O(nlogn)的算法可以解决本题,就是排序后只枚举r1。对于n个奶牛的位置,每个位置要么被第一个基站覆盖,要么被第二个基站覆盖,r1的取值也是从0到离它最远奶牛的距离,我们开始从最远的距离来枚举r1,此时第一个基站恰好能覆盖离它最远奶牛的位置;然后缩减r1的值到离它第二远奶牛的距离,这样一来离它最远奶牛的坐标只能由第二个基站覆盖了,就可以尝试更新r2了。依次类推,第一个基站每放出一个点,第二个基站就要主动接收它,直到全部放出所有的点让第二个基站接管。这种做法枚举r1的值只需要一重循环,对奶牛的坐标按照离第一个基站的距离从大到小排序需要O(nlogn)的时间,总的时间复杂度也就是O(nlogn)。这种做法为什么是正确的呢?暴力枚举时,对每个r1的取值,我们遍历所有的点,排除能被r1覆盖到的点,用剩下的点取更新r2,用了二重循环;而这种思路是一次放出一个不能被r1覆盖到的点,省去了我们遍历的过程,直接用不能被r1覆盖的点取更新r2,本质和暴力枚举是一样的。

代码

方法一:暴力枚举

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2005;
typedef pair<int,int> pii;
typedef long long ll;
ll d1[N],d2[N];
pii q[N];
ll dis(pii x,pii y){
    ll a = x.first - y.first;
    ll b = x.second - y.second;
    return a * a + b * b;
}
int main(){
    int n;
    cin>>n;
    pii z1,z2;
    cin>>z1.first>>z1.second>>z2.first>>z2.second;
    for(int i = 0;i < n;i++) {
        cin>>q[i].first>>q[i].second;
    }
    ll r1 = 0,r2 = 0;
    for(int i = 0;i < n;i++) {
        ll l1 = dis(z1,q[i]),l2 = dis(z2,q[i]);
        d1[i+1] = l1,d2[i + 1] = l2;
    }
    ll ans = 1e18;
    for(int i = 0;i <= n;i++) {//d1[0]表示r1取0
        r2 = 0;
        for(int j = 0;j <= n;j++) {
            if(d1[j] > d1[i]) {//超出lr1的范围
                if(r2 < d2[j])  r2 = d2[j];//更新r2
            }
        }
        ans = min(ans,d1[i] + r2);
    }
    printf("%lld\n",ans);
    return 0;
}

方法二:排序后枚举

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2005;
typedef pair<int,int> pii;
typedef long long ll;
pii q[N],z1,z2;
ll dis(pii x,pii y){
    ll a = x.first - y.first;
    ll b = x.second - y.second;
    return a * a + b * b;
}
bool cmp(pii x,pii y) {
    return dis(x,z1) > dis(y,z1);
}
int main(){
    int n;
    cin>>n;
    cin>>z1.first>>z1.second>>z2.first>>z2.second;
    for(int i = 0;i < n;i++) {
        cin>>q[i].first>>q[i].second;
    }
    sort(q, q + n,cmp);
    ll ans = 1e18,r2 = 0;
    for(int i = 0;i <= n;i++) {
        ans = min(ans,dis(z1,q[i]) + r2);
        r2 = max(r2,dis(z2,q[i]));
    }
    ans = min(ans,r2);
    printf("%lld\n",ans);
    return 0;
}

C AcWing 4430. 括号序列

题目描述

给定一个长度为 n 的括号序列 s,其中的第 i 个字符 si 要么是 (,要么是 )。

现在,你需要选择其中一个括号字符,并改变其括号类型(( 变为 ),) 变为 ()。

请你计算,有多少个位置 i 满足,将 si 的括号类型改变后,得到的新括号序列是一个合法括号序列。

如果新括号序列 s 同时满足:

s 中 ( 和 ) 的数量相同。
对于 s 的任意前缀字符串,其中包含的 ( 的数量均不少于 ) 的数量。
则新括号序列是一个合法括号序列。

输入格式
第一行包含整数 n。

第二行包含一个长度为 n 的由 ( 和 ) 组成的字符串。

输出格式
一个整数,表示满足条件的位置数量。

数据范围
前四个测试点满足 1≤n≤10。
所有测试点满足 1≤n≤106

输入样例1:
6
(((())
输出样例1:
3
输入样例2:
6
()()()
输出样例2:
0
输入样例3:
1
)
输出样例3:
0
输入样例4:
8
)))(((((
输出样例4:
0

分析

本题与第二题类似,没有考察具体的算法,但是思维难度也不小,考察分析问题和问题抽象的能力。

说到括号匹配,如果有多类括号,我们可以用栈来模拟括号匹配的过程来判断括号序列是否匹配。如果只有一类括号,就只需要用一个计数变量cnt进行模拟了,出现左括号,cnt++;出现右括号,cnt–。只要遍历的过程中cnt始终不为负数并且遍历完成后cnt的值是0,说明括号就是匹配的。

对于本题而言,改变其中一个括号的方向,使得最终的括号序列合法。括号序列合法,有一个必要条件就是左括号数量等于右括号数量。设原括号序列中左括号数量是p,右括号数量是q,如果改变一个左括号方向使得括号序列合法,则左括号数量少了1,右括号数量多了1,p - 1 = q + 1,p - q = 2,也就是左括号数量比右括号数量多2时,改变其中一个左括号的方向,才能让括号序列合法;同理如果q - p == 2,改变其中一个右括号的方向,也可能让括号序列合法。如果p和q的值相差不是2,那么一定不存在合法的方案。

上面这些思路还是比较容易想到的,比较难想到的是右括号比左括号多2个时改变右括号位置的方案以及左括号比右括号多2个时转化为右括号比左括号多的情况。如果我们先分析左括号比右括号多的情况,也不太好分析,只有以右括号比左括号多2个情况为突破口,才能更容易的求解本题。

以( ( ( ) ) ) ) )为例,左括号3个右括号5个,改变其中的一个右括号为左括号,括号序列即合法。回忆下我们前面说到的只有一类括号时如何判断括号是否匹配,出现左括号,cnt++,出现右括号,cnt–,需要遍历过程中cnt不为负数,否则说明已经遍历的序列中右括号比左括号多,不合法。我们遍历完前三个左括号,cnt = 3,再遍历三个右括号,cnt = 0,再次遍历一个右括号时,cnt = -1,不合法了,为了序列合法,只能在cnt变成-1之前将一个使得cnt下降的右括号改为左括号,cnt遇见第四个右括号才变成-1,所以前四个右括号任意一个变成左括号,cnt就不会变成-1,改变后左括号数量+1,右括号数量-1,原本是-1的cnt的值就变成了1,再遍历到最后一个右括号,cnt = 0,满足括号匹配规则。所以在q - p = 2时,我们可以模拟括号匹配的过程,一边遍历一边更新cnt和右括号数量q的值,一旦cnt变成-1,可以改变的右括号位置有q个,改变它,具体表现在cnt += 2,之后直接遍历后面的括号序列,一旦cnt再次小于0,说明不合法,返回0,否则遍历到最后,cnt变成0,返回q即可。

对于左括号比右括号多2个的情况,比如( ( ( ( ( ) ) ),能够让序列变成合法括号序列的左括号位置数等于((( )))))中能够让序列变成合法括号序列的右括号位置数,也就是将原括号序列做次翻转,reverser后左右括号方向都转过来,就将这种情况转化为了第一种情况了。

代码

#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
int count(string& s) {
    int cnt = 0,q = 0;
    for(int i = 0;i < s.size();i++){
        if(s[i] == '(') cnt++;
        else    cnt--,q++;
        if(cnt == -1) {
            cnt += 2;
            for(int j = i + 1;j < s.size();j++) {
                if(s[j] == '(') cnt++;
                else    cnt--;
                if(cnt < 0) return 0;
            }
            return q;
        }
            
    }
}
int main(){
    int n;
    cin>>n;
    string s;
    cin>>s;
    int p = 0,q = 0;
    for(int i = 0;i < s.size();i++) {
        if(s[i] == '(') p++;
        else    q++;
    }
    if(p - q != 2 && q - p != 2)    cout<<0<<endl;
    else{
        if(p > q) {
            reverse(s.begin(),s.end());
            for(int i = 0;i < s.size();i++){
                if(s[i] == '(')    s[i] = ')';
                else    s[i] = '(';
            }
        }
        cout<<count(s)<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值