题目列表
AcWing 4396. 取石子
题目描述
两个小朋友玩取石子游戏。
第一个小朋友面前有 n1 个石子,第二个小朋友面前有 n2 个石子。
两人轮流取自己面前的石子。
第一个小朋友先手,第二个小朋友后手。
第一个小朋友每轮次最多取 k1 个石子,最少取 1 个石子。
第二个小朋友每轮次最多取 k2 个石子,最少取 1 个石子。
率先取完自己面前石子的小朋友,视为失败。
请问,两个小朋友都采取最优策略的情况下,谁会获胜?
输入格式 一行,四个整数 n1,n2,k1,k2。
输出格式 如果第一个小朋友获胜,则输出 First,如果第二个小朋友获胜,则输出 Second。
数据范围 所有测试点满足 1≤n1,n2,k1,k2≤50。
输入样例1: 2 2 1 2 输出样例1: Second 输入样例2: 2 1 1 1 输出样例2: First
分析
本题考察博弈论里面的集合Nim游戏 (比大小)。率先取完的人输,所以每次最优策略是只取1个,两人都采取最优策略,那么初始石子数量多的人获胜,两人石子一样多,则后手获胜。
代码
#include <iostream>
using namespace std;
int main(){
int n1,n2,k1,k2;
cin>>n1>>n2>>k1>>k2;
if(n1 > n2) cout<<"First"<<endl;
else cout<<"Second"<<endl;
return 0;
}
AcWing 4397. 卡牌
题目描述
有 n 张卡牌,编号 1~n。
每张卡牌的正面和背面都各有一个数字。
第 i 张卡牌的正面数字为 ai,背面数字为 bi。
初始时,所有卡牌都正面朝上,显示正面的数字。
现在,你可以将其中一些卡牌翻面,使其显示背面的数字,要求:
至少有 k 张卡牌保持正面朝上。 所有卡牌显示的数字之和尽可能小。 输出所有卡牌显示的数字之和的最小可能值。
输入格式 第一行包含两个整数 n,k。
第二行包含 n 个整数 a1,a2,…,an。
第三行包含 n 个整数 b1,b2,…,bn。
输出格式 一个整数,表示所有卡牌显示的数字之和的最小可能值。
数据范围 前 6 个测试点满足 1≤n≤10。 所有测试点满足 1≤n≤2×105,0≤k≤n,1≤ai,bi≤104。
输入样例1: 3 1 5 4 6 3 1 5 输出样例1: 10 输入样例2: 5 3 3 4 7 10 3 4 5 5 12 5 输出样例2: 25
分析
本题考察贪心,初始n张卡牌正面朝上,要求翻转不超过n - k张卡牌使得显示的数字之和最小。翻转第i张卡牌,显示的数字之和将减小ai - bi。所以我们可以对a - b构成的数组进行排序,取其中最大的n - k张卡牌翻转,如果a - b中大于0的元素没有n - k个,就将所有大于0的数字翻转。
代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 200005;
int a[N],b[N];
int main() {
int n,k;
scanf("%d%d",&n,&k);
for(int i = 1;i <= n;i++) scanf("%d",&a[i]);
for(int i = 1;i <= n;i++) {
scanf("%d",&b[i]);
b[i] = a[i] - b[i];
a[i] += a[i-1];
}
sort(b+1, b + n + 1,[](int x1,int x2){return x1 > x2;});
for(int i = 1;i <= n - k;i++) {
if(b[i] <= 0) break;
a[n] -= b[i];
}
printf("%d\n",a[n]);
return 0;
}
AcWing 4398. 查询字符串
题目描述
给定 n 个字符串 f1,f2,…,fn。
这些字符串两两不同。
下面给定 q 个询问。
其中,第 i 次询问给定一个字符串 si,你的任务是:
计算 f1~fn 这 n 个字符串中,包含 si 作为子串的字符串的数量。 从 f1~fn 这 n 个字符串中,任选一个包含 si 作为子串的字符串输出。 输入格式 第一行包含整数 n。
接下来 n 行,其中第 i 行包含字符串 fi。
再一行包含整数 q。
接下来 q 行,其中第 i 行包含字符串 si。
所有 fi 和 si 都只包含小写字母、数字以及 .。
输出格式 共 q 行,其中第 i 行输出第 i 个询问的答案。
首先输出 f1~fn 这 n 个字符串中包含 si 作为子串的字符串的数量。
然后从 f1~fn 这 n 个字符串中任选一个包含 si 作为子串的字符串输出。
如果这样的字符串不唯一,则输出任意合理字符串均可,如果这样的字符串不存在,则输出 -。
数据范围 前三个测试点满足 1≤n,q≤20。 所有测试点满足 1≤n≤10000,1≤q≤50000,1≤|fi|,|si|≤8。
输入样例: 4 test contests test. .test 6 ts . st. .test contes. st 输出样例: 1 contests 2 .test 1 test. 1 .test 0 - 4 test.
分析
刚读完题第一反应就是Trie树,然后一顿操作唰唰写了Trie的插入,查询以及递归的查找合法字符串的三个函数,最后发现题目要查询的是子串而不是字符串的前缀,这意味着我们没法直接从Trie树的根节点开始进行查询,之后修改了好几轮才ac。 先介绍下简单的解法,使用Hash制表备查,由于字符串的长度都不会超过8,那么其子串的个数就不会超过8 + 7 + 6 +…+ 1=36,一共也就360000子串,不妨全部用哈希表存下来,这样一来就可以很快的求出包含子串的字符串的个数。注意一个字符串的子串里可能有重复的,需要去重,比如s = “aaaa”,长度为1的四个子串都是“a”,计数时自然不能都加上,所以需要对每个字符串的所以子串先进行去重,使用set即可实现。另外一个需要输出的是包含子串的任一个字符串,只需要在统计子串个数时再次使用hash表存储即可。 复杂度来看,Hash表的插入复杂度视为8,乘上36,再乘上10000,两百多万的复杂度足以很快的解决本题。 再介绍下Trie树的解法,前面说到,由于要查询的是子串,不方便利用Trie树查询的模板从头开始查找,其实只需要将字符串的部分子串也加入到Trie树里就可以解决了,比如s = “abc”;我们将abc、bc、c这三个s的后缀字符串都加入到Trie树里,这样一来,不论要查询的子串是什么,都可以查询得到了。一般Trie的插入操作会用cnt数组记录下以每个字符为结尾的字符串的个数,这里由于只需要前缀匹配就可以了,所以cnt改为记录以每个字符为结尾的子串所属字符串的个数。至于合法的字符串可以用一个字符串数组存下Trie树上每个字符所属的字符串,最后输出一下即可。Trie树的解法需要注意的细节不少,文字描述太过繁杂,需要注意的地方都以注释的形式写在Trie树解法的代码里了,看下代码会更快的理解。
代码
方法一:Hash + 制表备查
#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
using namespace std;
unordered_map<string,string> ans;
unordered_map<string,int> cnt;
int main(){
int n,q;
string s;
cin>>n;
while(n--) {
unordered_set<string> us;
cin>>s;
for(int i = 0;i < s.size();i++)
for(int j = 1;i + j <= s.size();j++)
us.insert(s.substr(i, j));
for(auto &x : us) {
cnt[x]++;
ans[x] = s;
}
}
cin>>q;
while(q--) {
cin>>s;
if(cnt.count(s)) cout<<cnt[s]<<" "<<ans[s]<<endl;
else cout<<"0 -"<<endl;
}
return 0;
}
方法二:Trie树
#include <iostream>
#include <string>
using namespace std;
//Trie树元素的上限:一个字符串至多8个后缀子串,每个子串至多在Trie树里插入8个字符,数据范围是1w,所以上限设置为36w。
const int N = 360005;
int idx,son[N][40],cnt[N];
string s,ans[N];
int transfer(char c) {
if(c >= 'a') return c - 'a';//小写字母映射到0-25
else if(c == '.') return 26;//.映射为26
else return c - '0' + 27;//数字映射从27开始
}
void insert(string subs) {
int p = 0;
for(int i = 0;i < subs.size();i++) {
int t = transfer(subs[i]);
if(!son[p][t]) son[p][t] = ++idx;//p节点不存在t字符的孩子节点则插入
p = son[p][t];
//字符串s的某个后缀子串第一次出现才增加计数,比如s = “aaa”,只有第一次插入aaa字符串才会增加节点上的计数,后面除非s更新了,
//进入到了后面字符串的插入才会继续更新,如果不这么写为了避免子串重复就需要像方法一那样插入所有子串去重后的字符串了
if(ans[p] != s) cnt[p]++;
ans[p] = s;//s定义为全局变量,每次cin都会更新,这样就不需要传参了,并且ans可以用来判断p节点是否已经计数了
}
}
int query(string s,int &p) {
for(int i = 0;i < s.size();i++) {
int t = transfer(s[i]);
if(!son[p][t]) return 0;
p = son[p][t];
}
return cnt[p];
}
int main(){
int n,q;
cin>>n;
while(n--) {
cin>>s;
insert(s);
for(int i = 1;i < s.size();i++) insert(s.substr(i));//不完整子串
}
cin>>q;
while(q--) {
cin>>s;
int p = 0;
int res = query(s,p);
cout<<res<<" ";
if(!res) cout<<"-"<<endl;
else cout<<ans[p]<<endl;
}
return 0;
}
|