题目
【问题描述】
小明想知道,满足以下条件的正整数序列的数量:
- 第一项为 n;
- 第二项不超过 n;
- 从第三项开始,每一项小于前两项的差的绝对值。
请计算,对于给定的 n,有多少种满足条件的序列。
【输入格式】
输入一行包含一个整数 n。
【输出格式】
输出一个整数,表示答案。答案可能很大,请输出答案除以10000的余数。
【样例输入】
4
【样例输出】
7
【样例说明】
以下是满足条件的序列:
4 1
4 1 1
4 1 2
4 2
4 2 1
4 3
4 4
【评测用例规模与约定】
对于 20% 的评测用例,1 <= n <= 5;
对于 50% 的评测用例,1 <= n <= 10;
对于 80% 的评测用例,1 <= n <= 100;
对于所有评测用例,1 <= n <= 1000。
思路
f(pre,cur) = f(cur,1) + f(cur,2) + … +f(cur,abs(pre-cur)-1) + 1
pre表示前一个数,cur代表当前的数,选定之后,序列种数等于以cur为前序,以1到abs-1为当前的序列数的总和再加1.
如f(5,2) = f(2,1)+f(2,2).
无优化(n=25时就已经将近1s了)
时间复杂度:n的三次方(第二个数最多接近n,想象成一条线。第三个数最多也接近n,此时可想成一个面。此后逐步嵌套,最多n次,此时可想成n个面构成一个高度为n的立方体。因此时间复杂度为n的三次方)
import java.util.Scanner;
public class Nine {
static final int MOD = 10000;
static int N;
static long ans;
static Scanner sc;
public static void main(String[] args) {
sc = new Scanner(System.in);
work();
}
static void work() {
ans = 0;
N = sc.nextInt();
long ago = System.currentTimeMillis();
for (int x = 1; x <= N; ++x) ans = (ans + dfs(N, x)) % MOD; //数列中的第二个数
System.out.println(ans);
System.err.println(System.currentTimeMillis() - ago);
}
static long dfs(int pre, int cur) {
// 询问状态
long ans = 1;
for (int j = 1; j < Math.abs(pre - cur); j++) { //数列中第三个到最后一个满足条件的数
ans = (ans + dfs(cur, j)) % MOD;
}
return ans;
}
}
优化1:记忆型递归(当n=600时大概是1s)
时间复杂度:仍然为n的三次方,只是对某些位置进行了记忆,减少了某些重复运算
import java.util.Scanner;
public class Nine {
static final int MOD = 10000;
static int N;
static long ans;
static Scanner sc;
static long[][] mem = new long[1001][1000];
public static void main(String[] args) {
sc = new Scanner(System.in);
work();
}
static void work() {
ans = 0;
N = sc.nextInt();
long ago = System.currentTimeMillis();
for (int x = 1; x <= N; ++x) ans = (ans + dfs(N, x)) % MOD; //数列中的第二个数
System.out.println(ans);
System.err.println(System.currentTimeMillis() - ago);
}
static long dfs(int pre, int cur) {
// 询问状态
if (mem[pre][cur] != 0) //优化1在这里,记忆型递归,因为相同的pre,cur被重复计算了很多次
return mem[pre][cur];
long ans = 1;
for (int j = 1; j < Math.abs(pre - cur); j++) { //数列中第三个到最后一个满足条件的数
ans = (ans + dfs(cur, j)) % MOD;
}
mem[pre][cur] = ans;
return ans;
}
}
优化2:每次解答树只展开两个节点,减少一层循环,虽然增加了递归,但是充分利用了记忆
时间复杂度:n的平方
解空间是N的平方(详细为N*N)表格,但是每次都要循环加总,所以成了N的立方,在同样的解空间下,避免循环加总,即可优化到N的平方
重新考虑状态的转移:
如果我们用f(i,j)表示前一个数是i,当前数是1到j的合法序列的个数;有f(i,j) = 1 + f(i,j-1) + f(j,abs(i-j)-1)即分为两个部分:
1)i作为前一个数,从1到j-1为当前数的合法序列的个数已经计算好
2)求以j为尾数,后面选择1到abs(i-j)-1的合法序列的个数。
如 f(10,5)=f(10,4)+f(5,4);而不是枚举1到5;这样每次解答树只展开两个节点,相当于减少一层循环,虽然解答树的层次还是很深,但是由于记忆的存在,解空间仍然是N的平方。可在100ms内解决。
import java.util.Scanner;
public class Nine {
static final int MOD = 10000;
static int N;
static long ans;
static long[][] mem = new long[1001][1000];
static Scanner sc;
public static void main(String[] args) {
sc = new Scanner(System.in);
work();
}
static void work() {
ans = 0;
N = sc.nextInt();
long ago = System.currentTimeMillis();
System.out.println(dfs(N, N)); //优化2:去掉了这里的循环
System.err.println(System.currentTimeMillis() - ago);
}
static long dfs(int pre, int cur) {
if (cur <= 0) return 0;
// 询问状态
if (mem[pre][cur] != 0)
return mem[pre][cur];
mem[pre][cur] = (1 + dfs(pre, cur - 1) + dfs(cur, Math.abs(pre - cur) - 1)) % MOD; //优化2:每次解答树只展开两个节点
return mem[pre][cur];
}
}