记忆化搜素和一般的状态转移方程各有优略,实际dp就是一种按简单循环规律的记忆化搜索,每次记录答案,两种算法的复杂度是相同的,各有优略;
记忆化搜索dfs,因为重复调用栈,复杂度和空间占用的较大,然而它可以完成一些无序搜索,已知状态转移方程但是路径太奇怪无法找到循环的规律,而且单点的效率比dp要快一些,dp是有些数据没有价值也被计算了;
dp的优势,一次都得到所有答案,没有调用栈的过程速度相对较快;
适合解决的问题类型:模拟,递归调用,求方案数;
2020蓝桥模拟赛B组第九题,
69
第九题 序列计数
题目
【问题描述】小明想知道,满足以下条件的正整数序列的数量:1. 第一项为 n;2. 第二项不超过 n;3. 从第三项开始,每一项小于前两项的差的绝对值。请计算,对于给定的 n,有多少种满足条件的序列。【输入格式】输入一行包含一个整数 n。【输出格式】输出一个整数,表示答案。答案可能很大,请输出答案除以10000的余数。【样例输入】4【样例输出】7【样例说明】以下是满足条件的序列:4 14 1 14 1 24 24 2 14 34 4【评测用例规模与约定】对于 20% 的评测用例,1 <= n <= 5;对于 50% 的评测用例,1 <= n <= 10;对于 80% 的评测用例,1 <= n <= 100;对于所有评测用例,1 <= n <= 1000。
思路:记忆型递归 O(N^3)
题干第三点,是一个递归定义,可以得到递归式:
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).
123
但是暴力递归的复杂度是指数级;
基本的优化方案是加状态记忆:输入1000时,实测运行时间为1000~2000ms;
参考代码
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <ctime>
第九题 序列计数
题目
【问题描述】小明想知道,满足以下条件的正整数序列的数量:1. 第一项为 n;2. 第二项不超过 n;3. 从第三项开始,每一项小于前两项的差的绝对值。请计算,对于给定的 n,有多少种满足条件的序列。【输入格式】输入一行包含一个整数 n。【输出格式】输出一个整数,表示答案。答案可能很大,请输出答案除以10000的余数。【样例输入】4【样例输出】7【样例说明】以下是满足条件的序列:4 14 1 14 1 24 24 2 14 34 4【评测用例规模与约定】对于 20% 的评测用例,1 <= n <= 5;对于 50% 的评测用例,1 <= n <= 10;对于 80% 的评测用例,1 <= n <= 100;对于所有评测用例,1 <= n <= 1000。
思路:记忆型递归 O(N^3)
题干第三点,是一个递归定义,可以得到递归式:
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).
123
但是暴力递归的复杂度是指数级;
基本的优化方案是加状态记忆:输入1000时,实测运行时间为1000~2000ms;
参考代码
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <ctime>
#define _for(i, x, y) for(register int i = x;i <= y;i++)
#define _fordw(i, x, y) for(register int i = x;i >= y;i--)
typedef long long LL;
using namespace std;
#define _fordw(i, x, y) for(register int i = x;i >= y;i--)
typedef long long LL;
using namespace std;
int N;
LL ans;
const int MOD = 10000;
int mem[1001][1000];
LL ans;
const int MOD = 10000;
int mem[1001][1000];
LL dfs(int pre, int cur) {
// 询问状态
if (mem[pre][cur] != 0)
return mem[pre][cur];
LL ans = 1;
_for(j, 1,abs(pre-cur) - 1) {
ans = (ans + dfs(cur, j)) % MOD;
}
//记录状态
mem[pre][cur] = ans;
return ans;
}
// 询问状态
if (mem[pre][cur] != 0)
return mem[pre][cur];
LL ans = 1;
_for(j, 1,abs(pre-cur) - 1) {
ans = (ans + dfs(cur, j)) % MOD;
}
//记录状态
mem[pre][cur] = ans;
return ans;
}
void work() {
ans = 0;
cin >> N;
// f(pre,cur) = sum(f(cur,_new))|_new from 1 to abs(pre-cur)-1
_for(x, 1, N) ans = (ans + dfs(N, x)) % MOD;
cout << ans << endl;
}
1234567891011121314151617181920212223242526272829303132333435
进一步优化
至此,能通过80%的数据(在1000ms限制下);
解空间是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内解决。
参考代码:
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <ctime>
ans = 0;
cin >> N;
// f(pre,cur) = sum(f(cur,_new))|_new from 1 to abs(pre-cur)-1
_for(x, 1, N) ans = (ans + dfs(N, x)) % MOD;
cout << ans << endl;
}
1234567891011121314151617181920212223242526272829303132333435
进一步优化
至此,能通过80%的数据(在1000ms限制下);
解空间是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内解决。
参考代码:
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <ctime>
typedef long long LL;
using namespace std;
using namespace std;
int N;
const int MOD = 10000;
int mem[1001][1000];
const int MOD = 10000;
int mem[1001][1000];
int dfs(int pre, int cur) {
if (cur <= 0) return 0;
if (mem[pre][cur] != 0) return mem[pre][cur];
return mem[pre][cur] = (1 + dfs(pre, cur - 1) + dfs(cur, abs(pre - cur) - 1)) % MOD;
}
if (cur <= 0) return 0;
if (mem[pre][cur] != 0) return mem[pre][cur];
return mem[pre][cur] = (1 + dfs(pre, cur - 1) + dfs(cur, abs(pre - cur) - 1)) % MOD;
}
void work() {
cin >> N;
cout << dfs(N, N) << endl;
}
cin >> N;
cout << dfs(N, N) << endl;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int a = clock();
work();
int b = clock();
clog << (b - a) << endl;
return 0;
}
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int a = clock();
work();
int b = clock();
clog << (b - a) << endl;
return 0;
}
而使用递归我们很难做到优化我只想到了第一种方法的递归:
我们dp[i][j],i代表当前数是几,j代表当前的有效数位,因为我们发现pre和cur确定时,他们的差只能时1到abs(pre-cur)-1,所以我为了统一,当前i=0时就算是一种,还有如果i==j时他们也只有一种;
我们不难发现状态转移方程 f[i][j]=f[0][abs(i-0-1)]+f[i][abs(i-1)-1]+.....+f[j][abs(i-j)-1)],无法调用for循环做