zoukankan      html  css  js  c++  java
  • 记忆化搜索和一般的动态规划

     

     

    记忆化搜素和一般的状态转移方程各有优略,实际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>
    #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;
    int N;
    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;
    }
    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>
    typedef long long LL;
    using namespace std;
    int N;
    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;
    }
    void work() {
        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;
    }

    而使用递归我们很难做到优化我只想到了第一种方法的递归:

    我们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循环做

  • 相关阅读:
    50
    49
    Windows编程之connect函数研究
    48
    C++创建窗口程序初步
    47
    46
    45
    计算机组成原理实验思路
    44(function pointer 2)
  • 原文地址:https://www.cnblogs.com/hjw201983290498/p/12574785.html
Copyright © 2011-2022 走看看