这场的题还不错,尤其是E题,FG偏简单。B是鸽巢原理,C要把几步操作看成一个整循环,然后加速递推,D没啥意思就是一个建图,E题写法很简单,但是思路需要把一列火车拆成两个时间点并分别处理,思路很精妙,F是博弈论SG函数,G是暴力模拟,set二分查找带个树状数组。
A - Cut
题面:
思路:
签到题,这题做法很多,总之说白了就是先打印后半段再打印前半段。
我假设这个数列是首尾相连循环的,那么就相当于要先打印第 − k -k −k 个元素(假设数列从 0 0 0 开始编号,其实也就是第 n − k n-k n−k 个),那么就从 − k -k −k 开始,每次边加一边打印就可以了。
code:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=105;
int n,k;
int a[maxn];
int main(){
cin>>n>>k;
for(int i=0;i<n;i++)cin>>a[i];
for(int i=0;i<n;i++)cout<<a[((i-k)%n+n)%n]<<" ";
return 0;
}
B - Decrease 2 max elements
题面:
给你一个由 N N N 个正整数 A = ( A 1 , A 2 , … , A N ) A = (A_1, A_2, \dots ,A_N) A=(A1,A2,…,AN) 组成的序列。高桥重复下面的操作,直到 A A A 只包含一个或更少的正整数元素:
- 将 A A A 按降序排序。然后将 A 1 A_1 A1 和 A 2 A_2 A2 减少 1 1 1 。
求他执行此操作的次数。
思路:
鸽巢原理。
如果 A 1 A_1 A1 这个最大的数足够大的话,那么它可以耗死后面所有的数,最后只留它一个。或者它不够大,最后这些数轮流抗刀最后只剩下一个 1 1 1 或者全 0 0 0。
假设最大数为 m x mx mx,总和为 t o t tot tot。 A 1 A_1 A1 这个最大的数足够大指的就是它大于了剩余数之和即 m x > t o t − m x mx>tot-mx mx>tot−mx,也就是 2 × m x > t o t 2\times mx>tot 2×mx>tot, m x mx mx 大于 t o t tot tot 的一半。这时会进行 t o t − m x tot-mx tot−mx 次运算。
如果不够大,那就大伙轮流抗刀,进行 t o t / 2 tot/2 tot/2 次,这里是整数除法,向下取整。
没卡long long,挺好。
code:
#include <iostream>
#include <cstdio>
using namespace std;
int n,mx,tot;
int main(){
cin>>n;
for(int i=1,t;i<=n;i++){
cin>>t;
mx=max(mx,t);
tot+=t;
}
if(mx*2>tot)cout<<tot-mx;
else cout<<tot/2;
return 0;
}
C - Triple Attack
题意:
你正在玩一个游戏。
有 N N N 个敌人排成一排,最前面的 i i i 个敌人的健康值是 H i H _ i Hi 。
你将使用初始化为 0 0 0 的变量 T T T 重复以下操作,直到所有敌人的生命值都变为 0 0 0 或更少。
- 将 T T T 增加 1 1 1 。然后,攻击最前方生命值大于等于 1 1 1 的敌人。如果 T T T 是 3 3 3 的倍数,则敌人的生命值会减少 3 3 3 ;否则,生命值会减少 1 1 1 。
当所有敌人的生命值变为 0 0 0 或更少时,求 T T T 的值。
思路:
可以发现造成的伤害值是以 1 , 1 , 3 1,1,3 1,1,3 为循环的,只是结尾可能凑不齐整循环。那么我们可以暴力做结尾(反正就几步的事), 然后中间的循环直接算出循环了几次正循环,加速一下就好了。
思路很简单,写的时候可能要调一阵子。
code:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=2e5+5;
int n,h[maxn];
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>h[i];
long long cur=0;
for(int i=1;i<=n;i++){
while(h[i]>0){
if(h[i]>=5){
cur+=h[i]/5*3;
h[i]%=5;
}
else {
cur++;
h[i]-=(cur%3)?1:3;
}
}
}
cout<<cur<<endl;
return 0;
}
D - Minimum Steiner Tree
题面:
给你一棵树,树上有 N N N 个顶点,编号为 1 1 1 至 N N N 。 第 i i i 条边连接顶点 A i A _ i Ai 和 B i B_i Bi 。
考虑从这个图中删除一些边和顶点(可能为零)后可以得到一棵树。求这样一棵树中包含所有 K K K 指定顶点 V 1 , … , V K V _1,\ldots,V _ K V1,…,VK 的顶点的最小数目。
思路:
假设树根必选,那么我们选某个节点就需要选上从根到这个节点之间的所有点和边,我们可以标记一下所有必选的顶点,然后把没有选上的点都删掉即可,答案就是剩余的顶点个数。
不过这个题没有指定根节点,也没说根一定必选,其实也好说,我们自己选一个指定点当根就行了。
code:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=2e5+5;
int n,k;
int head[maxn],ent;
struct edge{
int v,nxt;
}e[maxn<<1];
void add(int u,int v){
e[++ent]={v,head[u]};
head[u]=ent;
}
int fa[maxn],cnt;
bool vis[maxn];
void dfs(int u){
for(int i=head[u],v;i;i=e[i].nxt){
v=e[i].v;
if(v==fa[u])continue;
fa[v]=u;
dfs(v);
}
}
int main(){
// cin.tie(0)->sync_with_stdio(false);
cin>>n>>k;
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
add(u,v);
add(v,u);
}
int u;
cin>>u;
dfs(u);
vis[u]=true;
cnt++;
for(int i=2;i<=k;i++){
cin>>u;
while(!vis[u]){
vis[u]=true;
cnt++;
u=fa[u];
}
}
cout<<cnt<<endl;
return 0;
}
E - Train Delay
题面:
在 Atcoder 国家,有 N N N 座城市,编号为 1 1 1 至 N N N ,以及 M M M 列火车,编号为 1 1 1 至 M M M 。 i i i 次列车于 S i S_i Si 时从城市 A i A_i Ai 出发,于 T i T_i Ti 时到达城市 B i B_i Bi 。
给定一个正整数 X 1 X_1 X1 ,求一个非负整数 X 2 , … , X M X_2,\ldots,X_M X2,…,XM 的最小值 X 2 + … + X M X_2+\ldots+X_M X2+…+XM ,以满足下面的条件。
- 条件对于所有满足
1
≤
i
,
j
≤
M
1 \leq i,j \leq M
1≤i,j≤M 的一对
(
i
,
j
)
(i,j)
(i,j) ,若
B
i
=
A
j
B_i=A_j
Bi=Aj 和
T
i
≤
S
j
T_i \leq S_j
Ti≤Sj ,则
T
i
+
X
i
≤
S
j
+
X
j
T_i+X_i \leq S_j+X_j
Ti+Xi≤Sj+Xj 。
- 换句话说,对于任何一对原本可以换乘的列车,即使将每列列车 i i i 的出发和到达时间延迟 X i X_i Xi ,仍然可以换乘。
可以证明,这种将 X 2 , … , X M X_2,\ldots,X_M X2,…,XM 设为 X 2 + … + X M X_2+\ldots+X_M X2+…+XM 的最小值的方法是唯一的。
思路:
苦思冥想一上午,最后还是看了jiangly大佬录播的代码才会的,人家看一眼题目立马就会了,提键盘就写,太强了。
这个题的题意就比较晦涩,解释一下大概就是: B i = A j B_i=A_j Bi=Aj 指的就是第 i i i 辆火车的终点站等同于第 j j j 辆火车的起始站,并且 T i ≤ S j T_i \leq S_j Ti≤Sj 第 i i i 辆火车的到达时间早于第 j j j 辆火车的发车时间(意味着从第 i i i 辆火车下车的乘客可以在这换乘第 j j j 辆火车)。现在要求 T i + X i ≤ S j + X j T_i+X_i \leq S_j+X_j Ti+Xi≤Sj+Xj 第 i i i 辆火车延时 X i X_i Xi 后,让第 j j j 辆火车延时 X j X_j Xj,使得仍然可以满足第 i i i 辆火车的到达时间早于第 j j j 辆火车的发车时间(意味着仍然可以换乘)。
在一列火车 j j j 的起始站点上,需要满足它发车要慢于 以它为终点站,并且原本到站就早于它发车 的所有火车 i i i 的条件(其实只要晚于最晚到站的那列火车就可以了)。然后这列火车 j j j 就会在终点站产生一个新的到站时间。
我们发现一列火车的发车要看发车站的目前最晚到站时间,到站后再刷新一个到站时间。发车和到站是相对分离的关系,因此我们可以把一列火车的两个时间信息拆成发车时间和到站时间,然后排序,再设置一个当前的最晚到站时间。
当我们拿到一个发车时间时,前面的到站信息就都处理完了,我们看到的当前站点更新的就是目前最晚的到站时间了,可以直接算出这列列车的
X
X
X 值(也就是最小的延迟发车时间)。当我们拿到一个到站时间,这时候这列火车的
X
X
X 值一定被算出来了(因为到站时间一定不早于发车时间,相等时我们可以在排序时将发车时间排在前面,这样一对时间一定是发车在前),我们就可以直接算出这列火车的到站时间了。跑一遍就算出了所有
X
X
X 值。
不得不说真的神乎其技,瞬间就想到了这种相互耦合的关系,并得到了解法。排序时的处理更是天衣无缝。
code:
#include <iostream>
#include <cstdio>
#include <vector>
#include <array>
#include <algorithm>
using namespace std;
const int maxn=2e5+5;
int n,m,x[maxn],tm[maxn];
int A[maxn],B[maxn],S[maxn],T[maxn];
int main(){
cin>>n>>m>>x[1];
for(int i=1;i<=m;i++)cin>>A[i]>>B[i]>>S[i]>>T[i];
vector<array<int,3> > a;
for(int i=1;i<=m;i++){
a.push_back({S[i],1,i});
a.push_back({T[i],0,i});
}
sort(a.begin(),a.end());
for(auto [Time,o,i]:a){
if(o==1){
x[i]=max(x[i],tm[A[i]]-S[i]);
}
else {
tm[B[i]]=max(tm[B[i]],T[i]+x[i]);
}
}
for(int i=2;i<=m;i++)cout<<x[i]<<" ";
return 0;
}
F - Dividing Game
题面:
问题陈述
给你一个由
N
N
N 个正整数
A
=
(
A
1
,
A
2
,
…
,
A
N
)
A = (A_1, A_2, \dots ,A_N)
A=(A1,A2,…,AN) 组成的序列,其中每个元素至少是
2
2
2 。Anna
和 Bruno
用这些整数玩一个游戏。他们轮流执行以下操作,安娜先执行。
- 自由选择一个整数 i ( 1 ≤ i ≤ N ) i \ (1 \leq i \leq N) i (1≤i≤N) 。然后,自由选择 A i A_i Ai 的一个不是 A i A_i Ai 本身的正整数因子 x x x ,并用 x x x 代替 A i A_i Ai 。
不能进行操作的一方输,另一方赢。假定两位棋手都以最佳方式下棋,谁会获胜?
思路:
没什么特别的模型可套,还能用SG函数的题,基本就是SG函数题,用不了的可能是打表找规律。
对一个数可以用SG函数来求解。 1 1 1 是终止状态,因此 s g [ 1 ] = 0 sg[1]=0 sg[1]=0,用 s e t set set 存储每个数的所有因子的SG函数值。然后每枚举到一个数都算一下它的 m e x mex mex 值作为它本身的SG函数值,再枚举一下它的倍数,把它的SG值给到它的所有倍数,以此类推。
每次找完一个数可以清空它的 s e t set set 节省空间,不清问题应该也不大。平均一个数的因子个数是 log \log log 个,所以一共也就存 n log n n\log n nlogn 个数。
枚举每一个数的倍数的枚举次数是调和级数,即 n + n 2 + n 3 + ⋯ + + n n = n ∗ ( 1 1 + 1 2 + ⋯ + 1 n ) ≈ n log n n+\dfrac{n}2+\dfrac{n}3+\dots++\dfrac{n}n=n*(\dfrac{1}1+\dfrac{1}2+\dots+\dfrac{1}n)\approx n\log n n+2n+3n+⋯++nn=n∗(11+21+⋯+n1)≈nlogn 次。不会超时。
一个数的求出来的。对于多个数同时进行的 n i m nim nim 游戏,这一整个的局面的SG函数值就是将多个游戏的SG函数值全异或起来。
code:
#include <iostream>
#include <cstdio>
#include <set>
using namespace std;
const int maxn=1e5+5;
int sg[maxn];
set<int> SG[maxn];
void shai(){
sg[1]=0;
for(int i=2;i<=1e5;i++)SG[i].insert(sg[1]);
for(int i=2;i<=1e5;i++){
for(int j=0;;j++)
if(SG[i].find(j)==SG[i].end()){
sg[i]=j;
SG[i].clear();
break;
}
for(int j=2;j*i<=1e5;j++){
SG[j*i].insert(sg[i]);
}
}
}
int n;
int main(){
shai();
cin>>n;
int ans=0;
for(int i=1,t;i<=n;i++){
cin>>t;
ans^=sg[t];
}
puts((ans)?"Anna":"Bruno");
return 0;
}
不知道为什么只筛素数倍数也是对的,我不会证,有大佬会证可以在评论区赐教。
#include <iostream>
#include <cstdio>
#include <set>
using namespace std;
const int maxn=1e5+5;
bool vis[maxn*20];
int prime[maxn],sg[maxn],cnt;
set<int> SG[maxn];
void shai(){
sg[1]=0;
for(int i=2;i<=1e5;i++)SG[i].insert(sg[1]);
for(int i=2;i<=1e5;i++){
for(int j=0;;j++)
if(SG[i].find(j)==SG[i].end()){
sg[i]=j;
SG[i].clear();
break;
}
if(!vis[i]){
vis[i]=true;
for(int j=2;j*i<=1e5;j++){
SG[j*i].insert(sg[i]);
}
}
}
}
int n;
int main(){
shai();
cin>>n;
int ans=0;
for(int i=1,t;i<=n;i++){
cin>>t;
ans^=sg[t];
}
puts((ans)?"Anna":"Bruno");
return 0;
}
G - Add and Multiply Queries
题面:
给你长度为 N N N 的正整数序列 A A A 和 B B B 。请依次处理以下列形式给出的 Q Q Q 个查询。每个查询属于以下三种类型之一。
- 类型
1
1
1 :以
1 i x
的形式给出。将 A i A_i Ai 替换为 x x x 。 - 类型
2
2
2 :以
2 i x
的形式给出。用 x x x 代替 B i B_i Bi 。 - 输入
3
3
3 :以
3 l r
的形式给出。求解以下问题并打印答案。- 首先,设置 v = 0 v = 0 v=0 。依次将 i = l , l + 1 , . . . , r i = l, l+1, ..., r i=l,l+1,...,r 中的 v v v 替换为 v + A i v + A_i v+Ai 或 v × B i v \times B_i v×Bi 。最后求出 v v v 的最大可能值。
保证给定类型 3 3 3 查询的答案最多为 1 0 18 10^{18} 1018
思路:
只看形式的话其实很像线段树,但是常规做法没法完成操作 3 3 3。
可以发现一个性质,就是当 B i = 1 B_i=1 Bi=1 时,我们乘上它是没有意义的,还不如直接加 A i A_i Ai。但是如果要乘大于等于 2 2 2 的数的话,乘法的增长速度很快。因为保证给定类型 3 3 3 查询的答案最多为 1 0 18 10^{18} 1018,那么在查询区间内乘法最多不可能超过 60 60 60 次,查询大区间里必定大段都是 B i = 1 B_i=1 Bi=1。
那么我们可以暴力地计算每个 B i > 1 B_i>1 Bi>1 的地方(反正也就几十次),中间每一段 B i = 1 B_i=1 Bi=1 的段可以用树状数组或者线段树维护出 A i A_i Ai 的区间和来快速累加即可。
code:
#include <iostream>
#include <cstdio>
#include <set>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
ll n,q,a[maxn],b[maxn];
set<ll> S;
struct BIT{
int n;
vector<ll> tr;
void build(int _n){
n=_n;
tr.resize(n+1);
}
inline int lowbit(int x){return x&-x;}
void add(ll x,int id){
for(int i=id;i<=n;i+=lowbit(i))
tr[i]+=x;
}
ll query(int id){
ll ans=0;
for(int i=id;i;i-=lowbit(i))
ans+=tr[i];
return ans;
}
inline ll query(int l,int r){
if(l>r)return 0;
else return query(r)-query(l-1);
}
}tr;
int main(){
cin.tie(0)->sync_with_stdio(false);
cin>>n;tr.build(n);
for(int i=1;i<=n;i++){
cin>>a[i];
tr.add(a[i],i);
}
for(int i=1;i<=n;i++){
cin>>b[i];
if(b[i]>1)S.insert(i);
}
cin>>q;
for(int op,l,r;q;q--){
cin>>op>>l>>r;
if(op==1){
tr.add(-tr.query(l,l)+r,l);
a[l]=r;
}
else if(op==2){
if(b[l]==1 && r>1)S.insert(l);
else if(b[l]>1 && r==1)S.erase(l);
b[l]=r;
}
else {
int id;
ll ans=0;
while(true){
auto it=S.lower_bound(l);
if(it==S.end() || *it>r){
ans+=tr.query(l,r);
break;
}
id=*it;
// cout<<id<<endl;
ans+=tr.query(l,id-1);
ans=max(ans+a[id],ans*b[id]);
l=id+1;
}
cout<<ans<<endl;
}
}
return 0;
}