• AcWing 170. 加成序列[曾用名:加法链]


    \(AcWing\) \(170\). 加成序列 [加法链]

    题目传送门

    一、题目描述

    满足如下条件的序列 \(X\)(序列中元素被标号为 \(1、2、3…m\))被称为 加成序列

    \(X[1]=1\)
    \(X[m]=n\)
    \(X[1]<X[2]<…<X[m−1]<X[m]\)

    对于每个 \(k(2≤k≤m)\)都存在两个整数 \(i\)\(j\) \((1≤i,j≤k−1\)\(i\)\(j\) 可相等 ),使得 \(X[k]=X[i]+X[j]\)

    你的任务是:给定一个整数 \(n\),找出符合上述条件的 长度最小加成序列

    如果有多个满足要求的答案,只需要找出任意一个可行解。

    输入格式
    输入包含多组测试用例。

    每组测试用例占据一行,包含一个整数 \(n\)

    当输入为单行的 \(0\) 时,表示输入结束。

    输出格式
    对于每个测试用例,输出一个满足需求的整数序列,数字之间用空格隔开。

    每个输出占一行。

    二、题目解析

    注:在\(10\)步以内搜不到结果就算无解,之所以不能使用\(bfs\)来解决,是因为\(bfs\)会使用大量的空间,会\(MLE\)

    搜索规模 很深,但 答案深度 很浅,可以使用 迭代加深 来做

    顺序:依次考虑序列中的每个位置 \(1~ 2~ 3 ~4 ~,...\)

    搜索框架

    依次搜索序列中的每个位置\(u\),枚举\(i\)\(j\)作为分支,\(a[i]\)\(a[j]\)的和填到\(a[u]\)上,然后 递归填写下一个位置

    • 优化搜索顺序
      为了让序列中的数尽快逼近\(n\),在枚举\(i\)\(j\)从大到小 枚举

    三、疑惑问题

    \(Q1\):每次都枚举深度,深度不断\(++\),而每次又从头开始进行搜索,那会有很多进行重复搜索的呀?

    \(A\):因为深度越大,结点的数目也就越多,耗费的时间是呈几何级增长的,重复搜索的部分时间基本可忽略不计。

    \(Q2\):为什么会想到使用 迭代加深
    \(A\):看完题目第一反应求最短路:\(bfs\);看了一眼\(n=10000\),肯定会爆空间
    因此考虑\(dfs\),用\(dfs\)求最短路径,那不就是暗示我们使用 迭代加深 嘛~

    四、实现代码

    #include <cstdio>
    #include <cstring>
    
    using namespace std;
    const int N = 110;
    
    int n;     //终点值n
    int a[N];  //填充的每个数字,路径
    int depth; //最小长度上限
    
    // AC,53 ms
    
    // u:当前枚举到的位置,下标从1开始
    bool dfs(int u) {
        //如果走完最后一个位置,来到了一个尾巴哨兵面前,此时,需要检查刚刚最后填入的a[u-1]是不是等于数字n,是则找到了答案,
        //不是,则没有找到答案
        if (u == depth + 1) return a[u - 1] == n;
    
        for (int i = u - 1; i; i--)   //枚举所有的前序位置
            for (int j = i; j; j--) { //前序填充数字中,任意两个数的和,可以重复使用同一个数字
                int s = a[i] + a[j];  //两个数的和,这是一个可能要填充到u位置上的解
                // a数组是一个单调上升的序列,小于等于上一个位置的数字,都是不可能的分支,如果本次枚举的数字比前一个还小的话,那么肯定不是解。
                if (s <= a[u - 1]) return false;
                //超过上界的肯定不对,但是还有机会变小,所以是continue,而不是return false
                if (s > n) continue;
                //将s放入路径中
                a[u] = s;
                //在放完u之后,走到u+1的位置,那么这条路径是不是合法,不再依赖于自己,而是依赖于u+1这步的探测结果
                if (dfs(u + 1)) return true;
            }
    
        //如果所有组合都尝试了一遍,依然不可以找到true的答案,那么本路径无效
        return false;
    }
    
    int main() {
        // 题目要求:第1个数字是1,最后一个是n
        a[1] = 1;
        while (scanf("%d", &n) && n) { //多组测试数据,输入为0时停止,n是指序列的终止值
            depth = 1;                 //深度从1开始
            //迭代加深
            while (!dfs(2)) depth++; //从第2个位置开始搜索,不断放宽depth
            //输出搜索路径
            for (int i = 1; i <= depth; i++) printf("%d ", a[i]);
            puts("");
        }
        return 0;
    }
    

    五、玄学优化

    1. 最大值估值\(<\)目标值

    \(a[u]<=a[u-1]*2\),也就是说后一项 最多 是前一项的\(2\)倍,如果当前已知\(a[u-1]\)然后迭代加深的最大深度为\(depth\),则后面最多还有\(depth-u+1\)项,也就是说\(\large a[u-1]*2^{depth-u+1}<n\) 则在\(depth\)次之内 一定找不到答案

    2. \(a[n]\)必然由\(a[n-1]+?\)构成

    反证法: 假设最优解数列中 最后一个数 \(a[n]\) 不是由\(a[n - 1]\)转化而来,那么我们可以就可以去掉\(a[n - 1]\)得到 序列长度更加短 的答案,所以一定\(a[n]\)一定是由\(a[n-1]\) 转化而来。同理,其它\(a[n-1],a[n-2],...,a[2]\)均同此理。(数学归纳法)

    #include <cstdio>
    #include <cstring>
    
    using namespace std;
    const int N = 110;
    
    // AC,11ms
    
    int n;     //终点值n
    int a[N];  //填充的每个数字,路径
    int depth; //最小长度上限
    
    // u:当前枚举到的位置,下标从1开始
    bool dfs(int u) {
        //如果走完最后一个位置,来到了一个尾巴哨兵面前,此时,需要检查刚刚最后填入的a[u-1]是不是等于数字n,是则找到了答案,不是,则没有找到答案
        if (u == depth + 1) return a[u - 1] == n;
    
        for (int i = u - 1; i; i--) { //前序填充数字中,任意两个数的和,可以重复使用同一个数字
            int s = a[u - 1] + a[i];  //两个数的和,这是一个可能要填充到u位置上的解
    
            // 可行性剪枝
            // a数组必然是一个单调上升的序列,小于等于上一个位置的数字,都是不可能的分支
            //如果本次枚举的数字比前一个还小的话,那么肯定不是解,并且,后续的枚举只会越来越小,也不会成为答案
            if (s <= a[u - 1]) return false;
    
            // 估算值函数 减枝
            if (a[u - 1] * (1 << (depth - u + 1)) < n) return false;
    
            if (s > n) continue; //超过上界的肯定不对
    
            a[u] = s; //将s放入路径中
            //在放完u之后,走到u+1的位置,那么这条路径是不是合法,不再依赖于自己,而是依赖于u+1这步的探测结果
            if (dfs(u + 1)) return true;
        }
    
        //如果所有组合都尝试了一遍,依然不可以找到true的答案,那么本路径无效
        return false;
    }
    
    int main() {
        // 题目要求:第1个数字是1,最后一个是n
        a[1] = 1;
    
        while (scanf("%d", &n) && n) { //多组测试数据,输入为0时停止,n是指序列的终止值
    
            depth = 1; //深度从1开始
    
            //迭代加深
            while (!dfs(2)) depth++; //从第2个位置开始搜索,不断放宽depth
    
            //输出搜索路径
            for (int i = 1; i <= depth; i++) printf("%d ", a[i]);
            puts("");
        }
        return 0;
    }
    

    六、暴力\(BFS\)

    算法:\(BFS\)
    考虑暴力\(BFS\),通过队列实现从 初始状态 转移到 所有状态,每个状态用动态数组存下已枚举的合法序列,但是显然这样无论是 时间复杂度 还是 空间复杂度 都会爆炸。

    考虑先解决时间复杂度问题:
    一些重要的优化:
    设当前状态序列为\(a\),序列长度为\(siz\)
    由当前状态扩展新状态,枚举\(i,j\)(定义见题)时, 从大到小 枚举,这样到当前枚举的\(i,j\)不合法时可以直接退出,因为继续往下枚举的所有\(i,j\)都将不合法,即\(a[i] + a[j] <= a[siz]\)

    枚举得到的新序列可能出现重复,因为是相同新序列只能由唯一的原始序列扩展得到,所以在序列扩展时要排除重复的状态,用\(vis\)纪录一下即可

    每次扩展前,记录原始序列是否能得到新的答案,当答案数量达到\(100\)后退出,用\(cnt\)记录答案数量。

    考虑解决空间复杂度问题
    (重点)把队列中的所有状态类型设为\(short\) \(int\),因为序列元素大小上限只有\(100\),否则唯有\(MLE\)一途!

    #include <bits/stdc++.h>
    
    using namespace std;
    typedef short int int16; //使用短整型,以防止MLE
    const int N = 110;       //数组上限
    const int M = 100;       //极限值n的上限
    
    int n;     //输出以数字n结尾的加法链
    int cnt;   //已经完成预处理的数字个数,当n=M时,表示所有范围内的数字均已成功生成加法链
    
    queue<vector<int16>> q; //以动态数组记录状态
    vector<int16> ans[N];   //二维数组,第一维记录是哪个数,第二维是一个动态数组,描述第一维的数是怎么构建的加法链路径
    
    int main() {
        // 一、预处理出M以内的所有 加成序列
        // bfs
        q.push({1}); //从{1} 出发宽搜
        while (q.size()) {
            auto a = q.front();
            q.pop();
    
            int16 v = a[a.size() - 1]; //最后一位的填充值
    
            // 如果没有计算过,那么现在是第一次到达,最短路径,记录已计算过
            // 并且,记录 ans[v]的最短路径就是a这个动态数组
            if (!ans[v].size()) ans[v] = a, cnt++; // cnt++ :v这个数计算完成,数量++
    
            //从数组状态a开始扩展,下面的办法就统一扩展了一位,然后后面的循环中,都使用这一位
            vector<int16> x = a;
            x.emplace_back(0); // emplace_back()是c++11的新特性,可以理解为push_back的加强版本
    
            for (int i = a.size() - 1; i >= 0; i--) {
                int16 s = v + a[i];     // a[n]肯定由a[n-1]+?得到
                if (s > M) continue;    // 冒了肯定没用
                x[a.size()] = s;        // 构建新状态
                q.push(x);              // 入队列等待扩展
            }
            //已经计算完M个,可以结束了
            if (cnt == M) break;
        }
        // 二、输入n 后查表
        while (scanf("%d", &n) && n) {
            for (int i : ans[n]) printf("%d ", i);
            puts("");
        }
        return 0;
    }
    
  • 相关阅读:
    记录下首次开通流量主,开心开心
    微信小程序之本地缓存
    在使用ef的情况下,有Migrations文件,想要直接生成数据库
    CSS 设置圆角div和阴影效果
    小程序UI库(UI组件)
    没有找到可以构建的 NPM 包---小程序开发
    php 接口参数对象转数组方法
    tp5框架获取随机n条
    php图片上传base64接口上传
    php如何实现定时任务,php定时任务方法,最佳解决方案,php自动任务处理
  • 原文地址:https://www.cnblogs.com/littlehb/p/15988165.html
Copyright © 2020-2023  润新知