目录
A. Simply Strange Sort
题目链接
题意: 给你一个从
1
−
n
1-n
1−n的排列数组,
n
n
n的长度是奇数,按照题目给定的要求对序列进行变换,求多少次变换之后能够使得数组递增。
分析: 简单的模拟题,主要是要读懂题意, f ( i ) f(i) f(i)代表如果 a i > a i + 1 a_i > a_{i+1} ai>ai+1,则两者可以交换。每一次迭代变换,第i次的迭代规则:如果 i i i是奇数,对 1 , 3 , 5 , 7 , 9 1,3,5,7,9 1,3,5,7,9…奇数的位置的数字可以进行 f ( 1 ) , f ( 3 ) f(1),f(3) f(1),f(3)…的操作。如果i是偶数,对 2 , 4 , 6 , 8 , 10 2,4,6,8,10 2,4,6,8,10…偶数的位置可以进行 f ( 2 ) , f ( 4 ) f(2),f(4) f(2),f(4)的操作。按照题意模拟。
代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1005;
int a[N],n;
bool check(){
for(int i=1;i<=n;i++){
if(a[i]==i)continue;
return false;
}
return true;
}
int main()
{
int t;scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
int cnt=0,flag=0;
while(1){
if(check())break;
flag^=1;
int i=(flag?1:2);
for(;i<=n-1;i+=2){
if(a[i]>a[i+1])swap(a[i],a[i+1]);
}
cnt++;
}
printf("%d\n",cnt);
}
return 0;
}
B. Charmed by the Game
题目链接
题意: 这个题太难读了…就是说有两个人打球,玩家轮流发球,发球方赢了,就说这一方 holds;接球方赢了,就说这一方
b
r
e
a
k
s
breaks
breaks。题目告诉我们两人的得分
a
,
b
a,b
a,b。求可能的
b
r
e
a
k
s
breaks
breaks的次数,也就是求接球方可能的拿分次数,也就是后手赢球的可能次数。
分析: 知道了两人的得分
a
,
b
a,b
a,b,那么我们就能知道总的比赛场次是
a
+
b
a+b
a+b,先手的发球次数是
⌈
a
+
b
2
⌉
\lceil \frac{a+b}{2} \rceil
⌈2a+b⌉为
p
p
p,后手的发球次数是
⌊
a
+
b
2
⌋
\lfloor \frac{a+b}{2} \rfloor
⌊2a+b⌋为
q
q
q。
我们不知道谁先发球,先假设
A
l
i
c
e
Alice
Alice先发球,设
x
x
x为
A
l
i
c
e
Alice
Alice发球的失球个数(
0
≤
x
≤
p
0\leq x \leq p
0≤x≤p),
y
y
y为
B
o
r
y
s
Borys
Borys的失球个数(
0
≤
y
≤
q
0\leq y \leq q
0≤y≤q)。
如果
A
l
i
c
e
Alice
Alice先手发球,枚举
A
l
i
c
e
Alice
Alice先手发球的失球个数
x
x
x(也就是后手赢球)从
1
1
1到
p
p
p,因为有
a
=
(
p
−
x
)
+
y
a=(p-x)+y
a=(p−x)+y,所以
y
=
a
−
(
p
−
x
)
y=a-(p-x)
y=a−(p−x),只要(
0
≤
y
≤
q
0\leq y \leq q
0≤y≤q),这就是一种合法的两者比赛的结果策略。
k
k
k的值就是
x
+
y
x+y
x+y。
同理,如果
B
o
r
y
s
Borys
Borys先手发球,枚举
B
o
r
y
s
Borys
Borys先手发球的失球个数
y
y
y(也就是后手赢球)从
1
1
1到
p
p
p,因为有
b
=
(
p
−
y
)
+
x
b=(p-y)+x
b=(p−y)+x,所以
x
=
b
−
(
p
−
y
)
x=b-(p-y)
x=b−(p−y),只要(
0
≤
x
≤
q
0\leq x \leq q
0≤x≤q),这就是一种合法的两者比赛的结果策略。
k
k
k的值就是
x
+
y
x+y
x+y。
计算上述有多少个
k
k
k的值。
代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <set>
using namespace std;
int main()
{
int t;scanf("%d",&t);
while(t--){
int a,b;
scanf("%d %d",&a,&b);
int p=a+b+1>>1,q=a+b>>1;//先手发球的次数和后手发球的次数
set<int>ans;
for(int i=0;i<=p;i++){//枚举alice先手时,每一种状态
int x=i,y=a-(p-x);//x是alice先手输球也就是break,y是Borys先手输球
if(y>=0&&y<=q)ans.insert(x+y);
}
for(int i=0;i<=p;i++){//枚举Broys先手输球
int y=i,x=b-(p-y);
if(x>=0&&x<=q)ans.insert(x+y);
}
printf("%d\n",ans.size());
for(auto x:ans)printf("%d ",x);
printf("\n");
}
return 0;
}
C. Deep Down Below
题目链接
题意: 玩家控制一个英雄,英雄有一个力量值。英雄要进入n个洞穴,洞穴i有k_i个怪物
a
i
,
1
,
a
i
,
2
,
.
.
.
a
i
,
k
a_{i,1},a_{i,2},...a_{i,k}
ai,1,ai,2,...ai,k,只要进入了一个洞穴,就必须按照顺序一一打败他们。英雄能够打败他们只有英雄的力量值严格大于怪物的护甲值,如果打不过则游戏失败。英雄每打败一个怪物,英雄的力量值+1。求最小的护甲值,使得英雄可以按照一定顺序进入洞穴,并打败所有怪物。
分析: 贪心。首先,我们需要确定进入一个洞穴时,英雄最低需要多少的力量值。
因为英雄每打败一个怪物,英雄的护甲值
+
1
+1
+1。假设进入第一个洞穴英雄的力量值为
x
x
x,那么打完洞穴的第一个怪物,英雄的力量值变成
x
+
1
x+1
x+1,打完第二个怪物变成
x
+
2
x+2
x+2。所以只要保证对于第i个怪物的护甲值
y
i
y_i
yi,
x
+
i
−
1
>
y
i
x+i-1>y_i
x+i−1>yi即
x
>
y
i
−
i
+
1
x>y_i-i+1
x>yi−i+1。求出来该洞穴的
y
i
−
i
+
1
y_i-i+1
yi−i+1的最大值,就是要进入该洞穴的英雄至少需要的力量值。
明确了进入一个洞穴时,英雄最低需要多少的力量值,可以对每一个洞穴按照最低需要多少力量值进行排序,优先进入洞穴所需最低力量值小的洞穴,进入洞穴的顺序就定下来了。
因为每次打完怪物英雄的力量值会增加,所以我们自然而然地想到这样的贪心策略。优先攻击进入洞穴所需最低力量值小的洞穴,打完该洞穴后力量值会增加这个洞穴的怪物数量的值。我们可以二分最低力量值,判断哪个力量值是符合条件的。
也可以不使用二分,维护一个当前英雄力量值
s
u
m
sum
sum;走到当前洞穴之前,打败的怪物数量
c
n
t
cnt
cnt;英雄最低的过关力量值
a
n
s
ans
ans。
面对每一个洞穴,如果当前力量值大于等于进入洞穴所需的最低力量值,那么我们直接进入,并且更新sum为sum加上当前洞穴的怪物数量,然后更新
c
n
t
cnt
cnt;
如果当前力量值比进入洞穴所需最低力量值还要低,那么更新sum为进入当前洞穴所需的最低力量值+当前洞穴的怪物数量(因为打完这个洞穴之后的力量值至少是这个),更新ans为进入当前洞穴所需的最低力量值-cnt(因为进入这个洞穴之前,我们打败了
c
n
t
cnt
cnt个怪物,加了
c
n
t
cnt
cnt的力量值,因为当前洞穴所需的最低力量值是大于之前的力量值的,所以
a
n
s
−
c
n
t
ans-cnt
ans−cnt在之前的洞穴肯定也都满足)。
代码:
使用二分
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>
#define ll long long
using namespace std;
const int N=1e5+5;
int n;
struct node{
int maxx,num;
bool operator <(const node &a) const{
return maxx<a.maxx;
}
}q[N];
bool check(int x){
for(int i=1;i<=n;i++){
int maxx=q[i].maxx,num=q[i].num;
if(x>=maxx){
x+=num;
continue;
}
else return 0;
}
return 1;
}
int main()
{
int t;scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int k;scanf("%d",&k);
int maxx=0;
for(int j=1;j<=k;j++){
int x;scanf("%d",&x);
maxx=max(maxx,x-j+1);
}
q[i]={maxx+1,k};
}
sort(q+1,q+1+n);
int l=1,r=1e9+5,ans=1e9+5;
while(l<=r){
int mid=(l+r)/2;
if(check(mid))ans=mid,r=mid-1;
else l=mid+1;
}
printf("%d\n",ans);
}
return 0;
}
不用二分
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>
#define ll long long
using namespace std;
const int N=1e5+5;
struct node{
int maxx,num;
bool operator <(const node &a) const{
return maxx<a.maxx;
}
}q[N];
int main()
{
int t;scanf("%d",&t);
while(t--){
int n;scanf("%d",&n);
for(int i=1;i<=n;i++){
int k;scanf("%d",&k);
int maxx=0;
for(int j=1;j<=k;j++){
int x;scanf("%d",&x);
maxx=max(maxx,x-j+1);
}
q[i]={maxx+1,k};
}
sort(q+1,q+1+n);
ll sum=0,ans=0,cnt=0;
for(int i=1;i<=n;i++){
int maxx=q[i].maxx,num=q[i].num;
if(sum>=maxx)sum+=num;
else sum=maxx+num,ans=maxx-cnt;
cnt+=num;
}
printf("%lld\n",ans);
}
return 0;
}
D1. Up the Strip (simplified version)
题目链接
题意: 有一个长度为
n
n
n的垂直条,有一个令牌最初在
n
n
n的位置,你需要将它向上移动,最后移动到
1
1
1的位置。令牌在
x
(
x
>
1
)
x(x >1)
x(x>1)的位置时,有两种移位方式:
- 在 1 1 1和 x − 1 x−1 x−1之间选择一个整数 y y y,并将令牌从单元 x x x移动到单元 x − y x−y x−y。
- 在 2 2 2和 x x x之间选择一个整数 z z z,包括 x x x,然后把这个值从单元格 x x x移动到数组元素的 ⌊ x z ⌋ \lfloor \frac{x}{z} \rfloor ⌊zx⌋位置。
分析: 很明显的
d
p
dp
dp问题。第一种移位方式很好考虑,维护一个后缀和
s
u
f
suf
suf,记录后缀的所有方案数量。
主要是第二种移位方式如何考虑,如果暴力枚举,肯定会超时。这个时候我们用到整除分块(不了解的可以查一查),
⌊
x
z
⌋
\lfloor \frac{x}{z} \rfloor
⌊zx⌋有很多的z可能除完之后都是到达同一个点,而且这些z是连续的。这个时候整除分块可以快速求出到达同一个点的
z
z
z的范围,可以很大程度上减少我们的时间复杂度。
时间复杂度
O
(
n
n
)
O(n\sqrt n)
O(nn)
代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>
#define ll long long
using namespace std;
const int N=2e5+5;
ll dp[N];
int main()
{
int n,mod;scanf("%d %d",&n,&mod);
ll suf=0;
dp[n]=1;
for(int i=n;i>=1;i--){
dp[i]=(dp[i]+suf)%mod;
int k=2;
while(k<=i){
int m=i/(i/k);
dp[i/k]=(dp[i/k]+dp[i]*(m-k+1))%mod;
k=m+1;
}
suf=(suf+dp[i])%mod;
}
printf("%lld\n",dp[1]);
return 0;
}
D2. Up the Strip
题目链接
题意: 和D1题意一致,只是数据范围变大了
分析: D1的做法时间复杂度为 O ( n n ) O(n\sqrt n) O(nn),在这道题的数据下是肯定会 T L E TLE TLE的,所以我们需要用别的方法来优化,考虑到每一个状态由后继状态转移过来,比如说对于数x,可以由 2 x 2x 2x, 2 x + 1 2x+1 2x+1除以 2 2 2到达 x x x。对于每一个 x x x的除数 y y y,都能通过 [ x ∗ y , x ∗ y + y − 1 ] [x*y,x*y+y-1] [x∗y,x∗y+y−1]的数转移过来。所以我们每次枚举倍数,维护一个后缀和,时间复杂度减小为 O ( n l o g n ) O(nlogn) O(nlogn)
代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>
#define ll long long
using namespace std;
const int N=4e6+5;
ll dp[N],sum[N];
int main()
{
int n,mod;scanf("%d %d",&n,&mod);
dp[n]=sum[n]=1;
for(int i=n-1;i>=1;i--){
dp[i]=sum[i+1];//能够使用法一跳过来
for(int j=2;j*i<=n;j++){
int r=min((ll)n,(ll)i*j+j-1);//所有i*j+j-1都是除以j等于i的数
dp[i]=(dp[i]+sum[i*j]-sum[r+1])%mod;
}
sum[i]=(sum[i+1]+dp[i])%mod;
}
printf("%lld\n",dp[1]);
return 0;
}