第一题:平方数
在线测评链接:http://121.196.235.151/p/P1098
题目描述
ak机拿到一个整数 x x x,并希望通过如下两个操作将 x x x变为完全平方数。
- 如 x x x是素数,则将其减1
- 否则,将其除以自己最小的素因子。
ak机需要操作多少次?
输入描述
一个正整数 x ( 1 ≤ x ≤ 1 0 9 ) x(1\le x\le 10^9) x(1≤x≤109)
输出描述
一个整数,表示操作次数。
样例
输入
5
输出
1
输入
20
输出
3
思路:筛质数+优化技巧
本题我们需要快速判断一个数字 x x x是否是以下三种数字
- 质数:这里我们可以使用枚举 x x x的因子或者埃氏筛/线性筛的方式判断,复杂度分别为 O ( n ) O(\sqrt{n}) O(n)和 O ( 1 ) O(1) O(1),其中埃氏筛的预处理复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
但是这里有个问题,埃氏筛由于受限于时空间复杂度,一般最多只能判断 1 0 6 10^6 106以内的质数,因此本题需要采用一种优化技巧
对于 x ≤ 1 0 6 x\le 10^6 x≤106,我们可以使用埃氏筛在 O ( n l o g n ) O(nlogn) O(nlogn)的时间预处理,然后 O ( 1 ) O(1) O(1)的时间判断
对于 x > 1 0 6 x>10^6 x>106,我们直接使用枚举因子的方式判断,复杂度为 O ( n ) O(\sqrt{n}) O(n),具体如下:
bool check_prime(int x){ //当x>1e5的时候,判断x是否为质数,复杂度为sqrt(n)
for(int i=2;i*i<=x;i++){
if(x%i==0)return false;
}
return true;
}
- 完全平方数:直接利用库函数
sqrt
判断即可,内部库函数实际实现是利用二分或者牛顿迭代法,因此复杂度可以看成 O ( l o g n ) O(logn) O(logn) - 非质数的最小质因子:直接利用埃氏筛筛选后的素数表进行枚举即可
复杂度分析
对于操作一而言,复杂度最大为 O ( n ) O(\sqrt{n}) O(n),对于操作二而言, 1 0 5 10^5 105范围内的质数大概应该是几千左右,而且每经过一次操作2, x x x的值至少要除以2,因此最多执行操作2:30次( 2 30 > 1 0 9 2^{30}>10^9 230>109),因此总的时间复杂度一定是不会超时的。
C++
#include<bits/stdc++.h>
using namespace std;
const int N= 1e5+10;
int n;
set<int>primes;
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (st[i]) continue;
primes.insert(i);
for (int j = i + i; j <= n; j += i)
st[j] = true;
}
}
bool check_1(int x){ //判断x是否为完全平方数,复杂度为logn
int t=sqrt(x);
return t*t==x;
}
bool check_prime(int x){ //当x>1e5的时候,判断x是否为质数,复杂度为sqrt(n)
for(int i=2;i*i<=x;i++){
if(x%i==0)return false;
}
return true;
}
int main()
{
cin>>n;
get_primes(N);
int cnt=0;
while(!check_1(n)){
if((n<=1e5&&primes.count(n)||check_prime(n))){ //当前n是质数
n--;
cnt++;
}
else{
for(auto &t:primes){
if(n%t==0){
n/=t;
cnt++;
break;
}
}
}
}
cout<<cnt<<endl;
return 0;
}
Java
import java.util.*;
public class Main {
static final int N = (int)1e5 + 10;
static int n;
static Set<Integer> primes = new HashSet<>();
static boolean[] st = new boolean[N];
// 获取小于等于n的所有质数
static void getPrimes(int n) {
for (int i = 2; i <= n; i++) {
if (st[i]) continue;
primes.add(i);
for (int j = i + i; j <= n; j += i)
st[j] = true;
}
}
// 判断x是否为完全平方数,复杂度为logn
static boolean check1(int x) {
int t = (int)Math.sqrt(x);
return t * t == x;
}
// 当x>1e5的时候,判断x是否为质数,复杂度为sqrt(n)
static boolean checkPrime(int x) {
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return false;
}
return true;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
getPrimes(N-1);
int cnt = 0;
while (!check1(n)) {
if ((n <= 1e5 && primes.contains(n)) || checkPrime(n)) { // 当前n是质数
n--;
cnt++;
} else {
for (int t : primes) {
if (n % t == 0) {
n /= t;
cnt++;
break;
}
}
}
}
System.out.println(cnt);
}
}
Python
import math
N = int(1e5 + 10)
n = 0
primes = set()
st = [False] * N
# 获取小于等于n的所有质数
def get_primes(n):
for i in range(2, n):
if st[i]: continue
primes.add(i)
j = i + i
while j < n:
st[j] = True
j += i
# 判断x是否为完全平方数,复杂度为logn
def check_1(x):
t = int(math.sqrt(x))
return t * t == x
# 当x>1e5的时候,判断x是否为质数,复杂度为sqrt(n)
def check_prime(x):
i = 2
while i * i <= x:
if x % i == 0: return False
i += 1
return True
n = int(input())
get_primes(N)
cnt = 0
while not check_1(n):
if (n <= 1e5 and n in primes) or check_prime(n): # 当前n是质数
n -= 1
cnt += 1
else:
for t in primes:
if n % t == 0:
n //= t
cnt += 1
break
print(cnt)
第二题:跳格子
在线测评链接:http://121.196.235.151/p/P1099
题目描述
ak机小时候很喜欢一个叫跳格子的游戏,地上总共有
n
n
n几个格子,ak机跳到第
i
i
i个格子可以获得
a
i
a_i
ai的分数。
ak机从第1个格子开始,ak机每次可以跳跃一个悲波那契数的长度,她想知道她恰好跳到第
n
n
n个格子最多可以获得多少分。
所谓斐波那契数,指斐波那契数列:1,1,2,3,5,8.….中的某一项。悲波那契数列满足,第三项开始,每一项等于前两项之和。
输入描述
第一行输入一个整数 n ( 1 ≤ n ≤ 2 × 1 0 5 ) n(1\le n \le 2\times 10^5) n(1≤n≤2×105)表示数组长度。
第二行输入 n n n个整数表示每个格子的分数 a i ( − 1 0 9 ≤ a i ≤ 1 0 9 ) a_i(-10^9\le a_i\le 10^9) ai(−109≤ai≤109)
输出描述
输出一个整数表示答案:
样例1
输入
3
1 2 3
输出
6
说明
第1步,跳跃1格,跳到第2个格子
第2步,跳跃1格,跳到第3个格子。
获得的分数为6。
样例2
输入
3
1 -2 3
输出
4
思路:线性DP
这道题,其实就是跳跳棋的加强版,上面一道题每一次跳的格子,要么跳一格,要么跳两格,并且不能连续两次跳一格,这道题就是每次跳的格子可以自己选,但必须要是斐波拉契数列中的一项,那么根据上面一道题的状态转移方程,我们可以知道,如果有
m
m
m个斐波拉契数,数组长度
n
n
n,对应的时间复杂度为
O
(
n
×
m
)
O(n\times m)
O(n×m)
这样看上去似乎时间复杂度很大,有超时的风险,但是,我们要明白一个限制条件:跳跃的长度不可能超过数组的长度
n
n
n,其中
n
≤
2
×
1
0
5
n\le 2\times 10^5
n≤2×105,我们考虑,
≤
2
×
1
0
5
\le 2\times 10^5
≤2×105的斐波拉契数有几个,我们可以根据斐波拉契数列(二)的代码可以计算出,
≤
2
×
1
0
5
\le 2\times 10^5
≤2×105的斐波拉契数有28个,也就是说,
m
≤
28
m\le 28
m≤28
因此对应的时间复杂度
O
(
n
×
m
)
≤
O
(
2
×
1
0
5
×
28
)
=
O
(
5.6
×
1
0
6
)
O(n\times m)\le O(2\times 10^5\times 28)=O(5.6\times 10^6)
O(n×m)≤O(2×105×28)=O(5.6×106),这个是完全可以接受的,因此我们就结合斐波拉契数列(二)和跳跳棋的代码,来求解本题。
我们按照动态规划的三要素来分析这道题:状态方程定义、状态初始化、状态转移方程
状态方程定义
定义
f
[
i
]
f[i]
f[i]为跳到前
i
i
i个格子的最大得分
状态初始化
我们将起始位置视为1号位置,则有
f
[
1
]
=
w
[
1
]
f[1]=w[1]
f[1]=w[1]
状态转移方程
我们预先处理出最后一项不超过
n
n
n的斐波拉西数列
b
b
b,设
b
b
b数组的长度为
m
m
m
对于当前的位置
i
i
i,它上一步可以由
i
−
b
[
j
]
i-b[j]
i−b[j]跳过来,其中
0
≤
j
≤
m
−
1
0\le j\le m-1
0≤j≤m−1
因此有
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
i
−
b
[
j
]
]
+
w
[
i
]
)
f[i]=max(f[i],f[i-b[j]]+w[i])
f[i]=max(f[i],f[i−b[j]]+w[i])
最终输出
f
[
n
]
f[n]
f[n]即可
C++
#include<bits/stdc++.h>
using namespace std;
const int N=2E5+10;
int n,a[N];
long long f[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
vector<int>b={1,1}; //斐波那契数列
for(int i=2;b.back()<=n;i++){
b.push_back(b[i-1]+b[i-2]);
}
int m=b.size();
memset(f,-0x3f,sizeof f);
f[1]=a[1];
for(int i=2;i<=n;i++){
for(int j=1;j<m;j++){
int k=i-b[j];
if(k<0)break;
f[i]=max(f[i],f[k]+a[i]);
}
}
cout<<f[n]<<endl;
return 0;
}
Java
import java.util.*;
public class Main {
static final int N = (int)2E5 + 10;
static int n;
static int[] a = new int[N];
static long[] f = new long[N];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
for (int i = 1; i <= n; i++) a[i] = scanner.nextInt();
List<Integer> b = new ArrayList<>(Arrays.asList(1, 1)); // 斐波那契数列
for (int i = 2; b.get(b.size() - 1) <= n; i++) {
b.add(b.get(i - 1) + b.get(i - 2));
}
int m = b.size();
Arrays.fill(f, Long.MIN_VALUE);
f[1] = a[1];
for (int i = 2; i <= n; i++) {
for (int j = 1; j < m; j++) {
int k = i - b.get(j);
if (k < 0) break;
f[i] = Math.max(f[i], f[k] + a[i]);
}
}
System.out.println(f[n]);
}
}
Python
n = int(input())
a = [0] + list(map(int, input().split()))
b = [1, 1] # 斐波那契数列
while b[-1] <= n:
b.append(b[-1] + b[-2])
m = len(b)
f = [float('-inf')] * (n + 1)
f[1] = a[1]
for i in range(2, n + 1):
for j in range(1, m):
k = i - b[j]
if k < 0: break
f[i] = max(f[i], f[k] + a[i])
print(f[n])
第三题:2024快过去!
在线测评链接:http://121.196.235.151/p/P1100
题目描述
ak机非常喜欢一部 2023 年上线的作品,但是这部作品到 2025年才能有第二季,ak机觉得这个 2024 年不需要了,想直接结束2024年,因此她现在,常喜欢3和5但不喜欢4。
现在ak机有一个数组,她想知道这个数组中有多少个子序列的和是3和5的倍数,但不是4的倍数。
由于这个答案可能很大,因此你需要输一答案对 1 0 9 + 7 10^9+7 109+7取模后的结果。
输入描述
第一行输入一个整数 n ( 1 ≤ n ≤ 1 0 5 ) n(1\le n \le 10^5) n(1≤n≤105)表示数组长度。
第二行输入 n n n个整数 a ( − 1 0 9 ≤ a i ≤ 1 0 9 ) a(-10^9\le a_i\le 10^9) a(−109≤ai≤109)表示数组
输出描述
输出一个整数表示答案
样例
输入
3
13 30 17
输出
2
说明
有两个子序列满足要求:[13,17],[30]。
思路:动态规划+容斥定理
首先我们思考,什么样的数字是3的倍数,显然是对3取余结果为0的数字,即如果数字 x x x是3的倍数,则有 x % 3 = 0 x\%3=0 x%3=0
什么样的数字是5的倍数,显然是对5取余结果为0的数字,即如果数字 x x x是5的倍数,则有 x % 5 = 0 x\%5=0 x%5=0
那么问题来了,什么样的数字,既是3的倍数,又是5的倍数,显然,这个数字一定是15的倍数(因为3和5的最大公约数是1)
因此,我们先解决第一个问题,求出是3和5的倍数的个数 c n t 1 cnt1 cnt1
我们按照动态规划的三要素来分析这道题:状态方程定义、状态初始化、状态转移方程
状态方程定义
首先,所有的数字,对15取模的结果,一定是属于区间 [ 0 , 14 ] [0,14] [0,14]范围内的
因此,我们定义 f [ i ] [ j ] f[i][j] f[i][j]来表示选择前 i i i个数字,且数字对15取模的结果为 j j j的方案数
状态初始化
我们定义数组下标从1开始,一开始什么数字都不选,和为0,因此有 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1(表示空数组的情况)
状态转移方程
当我们枚举到数组的第 i i i个位置,有以下两种情况
- 不选择第 i i i个数字,则对于任意的 0 ≤ j ≤ 14 0\le j\le 14 0≤j≤14,有 f [ i ] [ j ] + = f [ i − 1 ] [ j ] f[i][j]+=f[i-1][j] f[i][j]+=f[i−1][j]
- 选择第 i i i个数字,我们设第 i i i个数字对15取模的结果为 x x x,则对于任意的 0 ≤ j ≤ 1 0\le j\le 1 0≤j≤1,有 f [ i ] [ ( j + x ) % 15 ] + = f [ i − 1 ] [ j ] f[i][(j+x)\%15]+=f[i-1][j] f[i][(j+x)%15]+=f[i−1][j]
最终有 c n t 1 = f [ n ] [ 0 ] cnt1=f[n][0] cnt1=f[n][0]
但本题还需要去把4的倍数的方案数给去除。
我们就需要考虑,什么数字 x x x,是3、4、5的倍数,显然,这个数字 x x x一定是60的倍数。
因此,根据容斥定理,我们还需要去除子序列中和是60的倍数的方案数。
我们按照上述的定义规则,定义 g [ i ] [ j ] g[i][j] g[i][j]表示选择前 i i i个数字,且数字对60取模的结果为 j j j的方案数
最终答案即为 f [ n ] [ 0 ] − g [ n ] [ 0 ] f[n][0]-g[n][0] f[n][0]−g[n][0]
C++
#include<bits/stdc++.h>
using namespace std;
const int N=1E5+10,mod=1e9+7;
int n,a[N];
long long f[N][15],g[N][60]; //f[i][j]/g[i][j]表示选择前i个数字,其中和%15/60的结果为j的方案数
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
f[0][0]=g[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<15;j++){
f[i][j]=(f[i][j]+f[i-1][j])%mod; //不选择第i个数字
f[i][(j+a[i])%15]=(f[i][(j+a[i])%15]+f[i-1][j])%mod; //选择第i个数字
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<60;j++){
g[i][j]=(g[i][j]+g[i-1][j])%mod; //不选择第i个数字
g[i][(j+a[i])%60]=(g[i][(j+a[i])%60]+g[i-1][j])%mod; //选择第i个数字
}
}
long long res=(f[n][0]-g[n][0]+mod)%mod;
cout<<res<<endl;
return 0;
}
Java
import java.util.Scanner;
public class Main {
static final int N = 100010;
static final int mod = (int)1e9+7;
static int n;
static int[] a = new int[N];
static long[][] f = new long[N][15];
static long[][] g = new long[N][60];
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i=1; i<=n; i++) a[i] = in.nextInt();
f[0][0] = g[0][0] = 1;
for(int i=1; i<=n; i++){
for(int j=0; j<15; j++){
f[i][j] = f[i-1][j] % mod; //不选择第i个数字
}
for(int j=0; j<15; j++){
f[i][(j+a[i])%15] = (f[i][(j+a[i])%15] + f[i-1][j]) % mod; //选择第i个数字
}
}
for(int i=1; i<=n; i++){
for(int j=0; j<60; j++){
g[i][j] = g[i-1][j] % mod; //不选择第i个数字
}
for(int j=0; j<60; j++){
g[i][(j+a[i])%60] = (g[i][(j+a[i])%60] + g[i-1][j]) % mod; //选择第i个数字
}
}
long res = (f[n][0] - g[n][0] + mod) % mod;
System.out.println(res);
}
}
Python
n = int(input())
a = list(map(int, input().split()))
N = 100010
mod = 10**9+7
f = [[0]*15 for _ in range(N)]
g = [[0]*60 for _ in range(N)]
f[0][0] = g[0][0] = 1
for i in range(1, n+1):
for j in range(15):
f[i][j] = f[i-1][j] % mod # 不选择第i个数字
for j in range(15):
f[i][(j+a[i-1])%15] = (f[i][(j+a[i-1])%15] + f[i-1][j]) % mod # 选择第i个数字
for i in range(1, n+1):
for j in range(60):
g[i][j] = g[i-1][j] % mod # 不选择第i个数字
for j in range(60):
g[i][(j+a[i-1])%60] = (g[i][(j+a[i-1])%60] + g[i-1][j]) % mod # 选择第i个数字
res = (f[n][0] - g[n][0] + mod) % mod
print(res)