A. Not a Substring
题意:
给定一个长度为 n n n 的括号序列 s s s,要求构造一个长度为 2 n 2n 2n 的正则括号序列 t t t 且 s s s 不是 t t t 的字串
判断是否能构造,如果可以构造就输出 t t t
思路:
- 如果 s s s 存在两个相邻且一样的字符,例如 s = ( ( ) s=(() s=(() 或 s = ( ) ) ( ) s=())() s=())(),那么这种情况我们可以构造 t = ( ) ( ) . . ( ) t=()()..() t=()()..() 这种形式,因为这样 t t t 里面找不到任何相等的两个相邻字符,且 t t t 是正则括号序列
- 否则 s s s 就是 ( ) ( ) ( ) ()()() ()()() 这种形式,可以令 t = ( ( ( ( ( ) ) ) ) ) t=((((())))) t=((((()))))
- 特别地:如果 s s s 长度为 2 2 2 并且 s = ( ) s=() s=() ,可以发现不存在对应的 t t t
我自己的赛时源码
// Problem: A. Not a Substring
// Contest: Codeforces - Educational Codeforces Round 153 (Rated for Div. 2)
// URL: https://codeforces.com/contest/1860/problem/0
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
bool solve(){
std::string s;
std::cin>>s;
int n=s.size();
if(n==2 && s[0]=='(' && s[1]==')') return false;
int idx=0;
while(idx<n && s[idx]=='(') ++idx;
while(idx<n && s[idx]==')') ++idx;
std::cout<<"YES\n";
if(idx==n){
fore(i,0,n) std::cout<<"()";
std::cout<<endl;
return true;
}
fore(i,0,n) std::cout<<"(";
fore(i,0,n) std::cout<<")";
std::cout<<endl;
return true;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--){
if(!solve()) std::cout<<"NO\n";
}
return 0;
}
参考答案用了一些 S T L STL STL 来实现,更加方便
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
void solve(){
std::string s;
std::cin>>s;
std::string a,b; //分别对应两种情况
int n=s.size();
fore(i,1,2*n+1){
a+=")("[i&1]; //奇数位( 偶数位(
b+=")("[i<=n];
}
if(a.find(s)==std::string::npos){ //s不是a的字串
std::cout<<"YES\n";
std::cout<<a<<endl;
}
else if(b.find(s)==std::string::npos){
std::cout<<"YES\n";
std::cout<<b<<endl;
}
else std::cout<<"NO\n";
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--) solve();
return 0;
}
B. Fancy Coins
题意:
M o n o c a r p Monocarp Monocarp 有两种硬币:
- 面值 1 1 1 元,有 a 1 a_1 a1 个常规币和无限个华丽币
- 面值 k k k 元,有 a k a_k ak 个常规币和无限个华丽币
现在他要买价值 m m m 元的物品,但是他不喜欢找钱,所以他给的硬币面值总和一定是 m m m
求出他最少可以使用多少个华丽币买下这个物品
思路:
这道题有两种做法,一种是三分,还有一种是数学推导
整数三分:
首先对于第二种硬币,它能使用的数量范围一定是
[
0
,
⌊
m
k
⌋
]
[0,\lfloor \dfrac{m}{k} \rfloor]
[0,⌊km⌋]
定义:
f
(
x
)
f(x)
f(x) 为使用
x
x
x 个第二种硬币时,使用的华丽币 数量
如果
f
(
x
)
f(x)
f(x) 是单峰函数,那么
x
∈
[
0
,
⌊
m
k
⌋
]
x \in [0,\lfloor \dfrac{m}{k} \rfloor]
x∈[0,⌊km⌋] 时
f
(
x
)
f(x)
f(x) 的最小值就是我们的答案
下面证明
f
(
x
)
f(x)
f(x) 是单峰函数:
考虑
f
(
x
+
1
)
−
f
(
x
)
f(x+1) - f(x)
f(x+1)−f(x) 的变化,从
f
(
x
)
f(x)
f(x) 到
f
(
x
+
1
)
f(x+1)
f(x+1) 就是把
k
k
k 个面值为
1
1
1 的硬币换成
1
1
1 个面值为
k
k
k 的硬币。显然,我们会先把面值为
1
1
1 的华丽币 丢弃,实在没有了才丢弃常规币。对于面值为
k
k
k 的硬币,我们优先取常规币,如果没有了,才会去取一个华丽币
那么可以发现丢弃的华丽币数量不增加,取的华丽币数量不减少,所以
f
(
x
+
1
)
−
f
(
x
)
f(x+1) - f(x)
f(x+1)−f(x) 不递减
而
f
(
0
)
f(0)
f(0) 和
f
(
⌊
m
k
⌋
)
f(\lfloor \dfrac{m}{k} \rfloor)
f(⌊km⌋) 显然大于等于最小值
证毕
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
int solve(){
int m,k,a1,ak;
std::cin>>m>>k>>a1>>ak;
int l=0,r=m/k;
while(r-l>2){
int mid1=l+(r-l)/3;
int mid2=r-(r-l)/3;
int res1=std::max(0,mid1-ak)+std::max(0,m-mid1*k-a1);
int res2=std::max(0,mid2-ak)+std::max(0,m-mid2*k-a1);
if(res1>res2) l=mid1;
else r=mid2;
}
int ans=INF;
fore(i,l,r+1) ans=std::min(ans,std::max(0,i-ak)+std::max(0,m-i*k-a1));
return ans;
}
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--) std::cout<<solve()<<endl;
return 0;
}
数学方法可以看 官方 T u t o r i a l Tutorial Tutorial
C. Game on Permutation
题意:
A
l
i
c
e
Alice
Alice 和
B
o
b
Bob
Bob 正在玩一个游戏,给定一个长度为
n
n
n 的排列,并且有一个芯片
它们轮流操作,
A
l
i
c
e
Alice
Alice 第一个操作,随意选择一个位置摆放芯片
在接下来的操作中,当前玩家只能将这个芯片移到现在这个元素的左边,并且那个位置的元素要小于当前位置的元素
如果当前的玩家无法操作,那么当前这个玩家胜出
定义: 第 i i i 个位置是 l u c k y lucky lucky 的当且仅当:
- A l i c e Alice Alice 一开始把芯片放在这个位置时,不管接下来 B o b Bob Bob 如何操作, A l i c e Alice Alice 均能获胜
输出这个排列有多少个 l u c k y lucky lucky 位置
思路:
我们对每个位置定义状态:
w
i
n
win
win 和
l
o
s
e
lose
lose
如果当前操作的玩家把芯片移动或摆放到这个位置,这个玩家的结局就是这个位置的状态
如果对于一个位置
i
i
i,找不到
j
<
i
j<i
j<i ,并且
p
j
<
p
i
p_j<p_i
pj<pi 的位置,那么这个位置对于
A
l
i
c
e
Alice
Alice 来说,一定是
l
o
s
e
lose
lose 的位置
否则,
B
o
b
Bob
Bob 就至少可以操作一次,如果前面有一个位置的状态是
w
i
n
win
win 的话,
B
o
b
Bob
Bob 一定会移到这个位置去,但是有一个前提:这个位置的
p
j
<
p
i
p_j<p_i
pj<pi
因此我们可以维护最小的状态为 w i n win win 的位置,如果发现 B o b Bob Bob 可以移动到那里的话,对于 A l i c e Alice Alice 来说,这个位置的状态就为 l o s e lose lose
// Problem: C. Game on Permutation
// Contest: Codeforces - Educational Codeforces Round 153 (Rated for Div. 2)
// URL: https://codeforces.com/contest/1860/problem/C
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int t;
std::cin>>t;
while(t--){
int n;
std::cin>>n;
std::vector<int> v(n+1);
fore(i,1,n+1) std::cin>>v[i];
int minv=n+1;
int minlose=n+1;
int ans=0;
fore(i,1,n+1){
bool win=true;
if(v[i]<minv){
win=false; //lose
minv=v[i];
}
else if(v[i]>minlose) win=false; //看看Bob能不能移到win位置
if(win) minlose=std::min(minlose,v[i]);
ans+=win;
}
std::cout<<ans<<endl;
}
return 0;
}
E. Fast Travel Text Editor
题意:
给定一个只包含小写字母的字符串 s s s,有一个光标,只能位于两字母之间,不能位于第一个字母之前或者最后一个字母之后
在一次操作中,你可以做一下三种移动之一:
- 往左移一格,但是不能在第一个字母之前
- 往右移一格,但是不能在最后一个字母之后
- 如果光标现在位置的左边字母是 x 1 x_1 x1,右边字母是 x 2 x_2 x2,那么可以移动到任意 x 1 − x 2 x_1-x_2 x1−x2 这样的相邻字母之间的位置
现给出 m m m 次询问,要求对于每次询问的 f f f 和 t t t,给出从 f − f + 1 f-f+1 f−f+1 中间这个位置到 t − t + 1 t-t+1 t−t+1 中间这个位置所需要的最少操作次数
思路:
我们可以考虑把这个问题建模成图论问题,一个有
n
−
1
n-1
n−1 个点(
n
n
n 个字母形成的
n
−
1
n-1
n−1 个空位)和若干条边的最短路问题
前两种移动方式其实就是从
i
i
i 到
i
+
1
i+1
i+1 或者从
i
+
1
i+1
i+1 到
i
i
i,类似于双向边
第三种移动就是:存在
i
→
j
i \rightarrow j
i→j 的边权为
1
1
1 的边,当且仅当
s
i
=
s
j
s_i = s_j
si=sj 且
s
i
+
1
=
s
j
+
1
s_{i+1} = s{j+1}
si+1=sj+1
在建完图之后,对于每个询问,我们只要求出 f → t f \rightarrow t f→t 的最短路长度即可,但是如果 m m m 次询问,每次都跑一次最短路算法的话,时间复杂度会很高,并且对于第三种操作的边,在一些比较特别的样例,要增加的边的数量很可能达到 C n 2 C_n^2 Cn2,空间复杂度也会很高
其实,第三种操作的点,一定是可以互相到达的,也就是说,如果 A A A 可以跳到 B B B , B B B 可以跳到 C C C,那么其实 A 、 B 、 C A、B、C A、B、C 三个点都可以互相通过一个边权为 1 1 1 的边到达,这时候可以建一个虚点,把这些可以互相到达的连通块,链接起来,我们可以设置几条从 A 、 B 、 C A、B、C A、B、C 进入虚点的有向边,边权为 0 0 0,再设置几条离开虚点到 A 、 B 、 C A、B、C A、B、C 的有向边,边权为 1 1 1,这样不管怎么跳,经过一次虚点就相当于经过了一条边权为 1 1 1 的边,与操作三是等价的
这样子设置的虚点最多有 26 ∗ 26 26*26 26∗26 个,因为选 26 26 26 个字母作为第一个,再选 26 26 26 个字母作为第二个,光标放在中间。这样子多加的边的数量不会超过 2 n 2n 2n ,与原来的 C n 2 C_n^2 Cn2 相比,大大减少
再来考虑时间复杂度的问题,发现从
f
f
f 到
t
t
t 的最短路,要么直接左右走过去,要么经过若干个虚点,途中可能还要有一些左右走的操作。
那么我们不妨把答案初始化成左右走需要的步数,然后枚举所有的虚点,求出它们各自到其他所有点的最短距离
设从当前这个虚点出发,到
i
i
i 点的最短距离是
d
[
i
]
d[i]
d[i],那么如果
f
→
t
f \rightarrow t
f→t 的最短路要经过这个虚点的话,
f
→
t
f \rightarrow t
f→t 的最短路长度就是
d
[
f
]
+
d
[
t
]
d[f] + d[t]
d[f]+d[t]
每一次枚举的虚点,用 d d d 数组对所有询问取 m i n min min 值
其实这里还有一个小优化: 01 BFS
由于图里面的边权只有
0
0
0 或
1
1
1,因此可以把
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra 的优先队列换成双端队列
如果这个边权为
0
0
0 就把这个更新的点插入到队头,否则插入到队尾
相当于先考虑边权为
0
0
0 的边,这样一定是更优的
这样子的时间复杂度其实是:
O
(
n
)
O(n)
O(n) ,因为每个点只会被更新一次,非常快
时间复杂度: O ( 2 6 2 ⋅ ( n + 2 6 2 + q ) ) \quad O(26^2\cdot(n+26^2+q)) O(262⋅(n+262+q))
// Problem: E. Fast Travel Text Editor
// Contest: Codeforces - Educational Codeforces Round 153 (Rated for Div. 2)
// URL: https://codeforces.com/contest/1860/problem/E
// Memory Limit: 512 MB
// Time Limit: 5000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define fore(i,l,r) for(int i=(int)(l);i<(int)(r);++i)
#define fi first
#define se second
#define endl '\n'
const int INF=0x3f3f3f3f;
const long long INFLL=0x3f3f3f3f3f3f3f3fLL;
typedef long long ll;
const int AL=26; //26个字母
const int N=50005;
struct edge{
int to;
int w;
};
struct ask{
int f;
int t;
int ans;
};
int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
std::string s;
std::cin>>s;
int n=s.size();
int m;
std::cin>>m;
std::vector<ask> query(m);
fore(i,0,m){
std::cin>>query[i].f>>query[i].t;
--query[i].f;
--query[i].t;
query[i].ans=std::abs(query[i].f-query[i].t); //初始化答案为两点距离
}
int v_num=n-1+AL*AL; //节点数量 最多只有26*26个虚点
std::vector<edge> g[v_num];
fore(i,0,n-1){
if(i){
g[i].push_back({i-1,1}); //左右走
g[i-1].push_back({i,1});
}
int j=n-1+(s[i]-'a')*AL+(s[i+1]-'a'); //虚点编号 26进制
g[i].push_back({j,0}); //去虚点边权为0
g[j].push_back({i,1}); //出虚点边权为1
}
fore(st,n-1,v_num){ //枚举虚点
std::vector<int> d(v_num,INF); //距离数组
d[st]=0;
std::deque<int> q;
/* 01 BFS */
q.push_front(st);
while(!q.empty()){
int u=q.front();
q.pop_front();
for(auto it:g[u]){
int v=it.to,w=it.w;
if(d[u]+w<d[v]){
d[v]=d[u]+w;
if(w==0) q.push_front(v); //边权为0的插到队头
else q.push_back(v); //边权为1插到队尾
}
}
}
for(auto& it:query) it.ans=std::min(it.ans,d[it.f]+d[it.t]-1);
// 这里-1是因为虚点->f 会比 f->虚点 多1个长度,因为离开虚点边权为1 进入虚点边权为0
}
fore(i,0,m) std::cout<<query[i].ans<<endl;
return 0;
}