• 分享非线性动态规划问题(一)


    摘要:前面的都是简单的线性动规,本篇开始树形动规问题的入门.

    1.正文:树是我最喜欢的数据结构(没有之一),它既不是像线性表那样过于单调的线性关系,又没有像图那样有着复杂的网状关系,规律的非线性关系常常是我们现实生活中诸多问题的基本模型,比如家族关系、企业架构等。当它与动态规划相结合时,似乎很高大上,但一言概之只是用一个树状的数据结构封装问题元素的动规问题而已,仍然是按动规的基本步骤走:

      (1)题意分析;

      (2)基于分析数学建模;

      (3)判定是否可以符合使用动规的两大前置条件(最优子结构和无后效性),是则下一步,否则终止(非动规可以解决的问题,另寻他法);

      (4)动规基本三步曲:

        1)结合题意根据模型选择计算出比较合适的状态转移方程,归约初始的状态值,推导出终止(最终收敛)条件;

        2)迭代验证;

        3)选择合适的迭代次序实现状态转移方程的迭代和收敛;

      (5)编程实现。

      万变不离其宗。不多赘述,看看今日有趣的例题。

    2.题目:

    经典例题(某度就有)

    没有上司的晚会等
    【问题描述】
    有个公司要举行一场晚会。为了让到会的每个人不受他的直接上司约束而能玩得开心,公司领导决定:如果邀请了某个人,那么一定不会再邀请他的直接的上司,但该人的上司的上司,上司的上司的上司……都可以邀请。已知每个人最多有唯一的一个上司。
    已知公司的每个人参加晚会都能为晚会增添一些气氛,求一个邀请方案,使气氛值的和最大。

    3.输入输出示例:

    输入:

    第1行一个整数N(1<=N<=6000)表示公司的人数。
    接下来N行每行一个整数。第i行的数表示第i个人的气氛值x(-128<=x<=127)。
    接下来每行两个整数L,K。表示第K个人是第L个人的上司。
    输入以0 0结束。

    输出:  

    5

    4.例程: 

     
    package algorithm;

    import java.util.*;

    /**
    * @Project: texat
    * @Author: h
    * @Package: algorithm
    * @Version: V1.0
    * @Title:
    * @Tag:
    * @Description:
    * @Date: 2020/6/9 23:40
    */
    public class TreeDynamicSolution {

    private static class TreeNode {
    //父节点
    TreeNode p = null;
    //子节点
    List<TreeNode> c = new ArrayList<>();
    //父节点不来的最大气氛总值
    int offVal = 0;
    //父节点来的最大气氛总值
    int onVal;

    public TreeNode(int onVal) {
    this.onVal = onVal;
    }
    }

    public static void mainByTreeNode(String[] strings) {
    //建树
    HashMap<Integer, TreeNode> no2TreeNode = new HashMap<>();
    HashSet<Integer> parentSet = new HashSet<>();
    Scanner scanner = new Scanner(System.in);
    int len = scanner.nextInt();
    for (int i = 0; i < len; i++) {
    int val = scanner.nextInt();
    no2TreeNode.put(i + 1, new TreeNode(val));
    parentSet.add(i + 1);
    }
    int targetVal = 0, parentVal = 0;
    while (scanner.hasNextInt() && (targetVal = scanner.nextInt()) != 0) {
    parentVal = scanner.nextInt();
    no2TreeNode.get(targetVal).p = no2TreeNode.get(parentVal);
    no2TreeNode.get(parentVal).c.add(no2TreeNode.get(targetVal));
    parentSet.remove(targetVal);
    }
    //计算气氛值
    Integer root = parentSet.iterator().next();
    TreeNode treeNode = no2TreeNode.get(root);
    sumMaxHappy(treeNode);
    //输出
    System.out.println(Math.max(treeNode.offVal, treeNode.onVal));
    }

    /**
    * 链式存储:树节点递归
    * @param root
    * @return
    */
    private static void sumMaxHappy(TreeNode root) {
    if (null == root) {
    return;
    }
    root.c.forEach(c -> {
    sumMaxHappy(c);
    root.offVal += Math.max(c.offVal, c.onVal);
    root.onVal += c.offVal;
    });
    }


    private static int[][] f;
    private static boolean[][] link;
    private static int[] happyVal;

    public static void mainByArray(String[] strings) {
    //申请数组空间
    Scanner scanner = new Scanner(System.in);
    int count = scanner.nextInt();
    link = new boolean[count][count];
    happyVal = new int[count];
    f = new int[count][2];
    boolean[] isNotRoot = new boolean[count];
    //录入
    for (int i = 0; i < count; i++) {
    int val = scanner.nextInt();
    happyVal[i] = val;
    }
    int target, parent;

    while (scanner.hasNextInt() && (target = scanner.nextInt()) != 0) {
    parent = scanner.nextInt();
    isNotRoot[target - 1] = true;
    link[parent - 1][target - 1] = true;
    }
    int root = -1;
    for (int i = 0; i < count; i++) {
    if (!isNotRoot[i]) {
    root = i;
    break;
    }
    }
    //计算气氛值
    sumMaxHappy(root);
    int max = Math.max(f[root][1], f[root][0]);
    //输出
    System.out.println(max);
    }

    /**
    * 顺序存储:数组版本
    * @param root
    */
    private static void sumMaxHappy(int root) {
    if (root < -1) {
    return;
    }
    boolean[] links = link[root];
    //初始值
    f[root][1] = happyVal[root];
    for (int i = 0; i < links.length; i++) {
    if (links[i]) {
    sumMaxHappy(i);
    f[root][0] += Math.max(f[i][1], f[i][0]);
    f[root][1] += f[i][0];
    }
    }
    }
    }


     

    现在按基本套路捋一遍:
    a.分析题意并基于分析建模:此题还是相对容易建模的,因为题干有显式表达的树状上下级关系,可以很容易地抽象出树这个状态存储的数据结构(其实之前线性的动规也是有存储的数据结构,即最简单的数据结构--数组,这也不容忽略,因为什么样的计算机计算都不离开一个存储数据的容器),在树的基础上我们可以进一步有子状态节点与父状态节点的状态转移方程。须说明的是,为什么一定要在树的基础上去推导这次的状态转移方程?

    归根结底有以下原因:

    1)父状态与多个子状态的决策结果(即子状态对应的子树的总气氛值)有迭代关联,即一个前驱与多个后继的关系,就是需要用树这种数据结构的存储状态了;

    2)对比以往线性动规的情况,你会发现这里有求总和的操作,这就是要求每个子节点都要参与到最终结果当中了,所以不可能像线性那样只选择一个带特征值(特征值即最大值最小值之类的)子节点推出父节点。

    b.满足两大特征:

    1)最优子结构:子树的最大总气氛值可以推出父节点对应的树的最大总气氛值;

    2)无后效性:历史的子树的最大总气氛值选择(本题可以说不存在选择,因为所有子树都与父节点迭代相关)并不会影响后面的上层节点的计算。


    c.状态转移三步走:
    1) f[i][0]表示i不来时的最大气氛总和,f[i][1]表示i来时的最大气氛总和,显然,状态转移方程可以基于模型得出,分类讨论一下即可:12)

      (1)上级与会:f[p][1] = sum{f[c][0]};

      (2)上级不与会:f[p][0] = sum{Max{f[c][1], f[c][0]}};

      其中p是所有c的父节点,一直推导到根节点,两者比较最大者胜出:s = Max{f[root][1], f[root][0]},这就”打擂台“得到了最大气氛总和了。

      显然,初始值f[i][1] =a[i],f[i][0] = 0, 终止条件是遍历完成;

    2)迭代的验证:不加赘述,可自行验证。3)迭代次序:须注意,树有两类遍历顺序,自顶向下,自底向上,为了得到最终根节点的答案,我们基于以上的方程是要做自底向上的推导的,因为父节点的迭代是要基于子树已经得到最终结果的情况下计算才正确。


    d.编程实现:见上面程序的两种实现:顺序存储的容器存储和链式存储的容器存储。

    5.总结:

     树,大家耳熟能详的一种数据结构;动态规划,大家经常见到的一类题型。当两者相结合时,恰恰是考验我们是不是有基于已有知识进行推导演绎的真工夫:

    1)之前说过,对于未曾建模的题型,建模将会是难点,事实亦是如此,考验的是你的数学抽象能力;

    2)推导选择出合适的状态转移方程,无他,唯训练尔,万丈高楼平地起。

    还是要强调一下,做题只是术,我们实则都是求道者。自始至终在解决千篇一律的海量题目的,是我们的思维,而不是所谓的战术,不要被条条框框给限制住,跳出来看问题,别人觉得的只是别人觉得的,你需要自己去感知。跳出来,就知道,这并不难。

  • 相关阅读:
    CodeForces
    HihoCoder
    HihoCoder
    CodeForces
    CodeForces
    CodeForces
    HihoCoder
    HihoCoder
    CodeForces
    HihoCoder
  • 原文地址:https://www.cnblogs.com/kentkit/p/13082765.html
Copyright © 2020-2023  润新知