加分二叉树(信息学奥赛一本通- P1580)(洛谷-P1040)

一、 题目分析

在信息学奥赛的早期真题中,NOIP 2003 的《加分二叉树》是一道具有代表性的好题。题面看似是在考查二叉树的构建与遍历,但它却给出了一个很致命、也是破局核心的条件: "二叉树的中序遍历为 (1, 2, 3, ..., n)"

在数据结构中,中序遍历的顺序是"左子树 → 根 → 右子树"。既然整棵树的中序遍历是连续的 1 到 n,这就意味着一个物理定律:对于树上的任意一棵子树,它所包含的所有节点编号,在物理上绝对构成一段连续的区间 [i,j]!

一旦清楚这一点,这道题的图论外衣就被彻底扒下,露出了它区间DP的真实面目。

二、 核心状态定义与转移方程

既然是处理连续区间,我们直接套用区间DP的经典模型:

  • 状态定义:设 dp[i][j] 表示由编号i到j的节点所组成的子树,能获得的最高加分。

  • 记号本(状态溯源) :题目不仅要求最高分,还要求输出前序遍历。我们额外开一个数组 root[i][j],记录区间 [i,j] 取得最高分时,是哪个节点k当了树根。

转移策略(枚举断点打擂台): 对于区间 [i,j],我们不知道谁当根节点收益最大。因此,我们让区间内的每一个节点 k (i≤k≤j) 都轮流"坐庄"当一次根节点。 当k为根时,区间被完美切割:左子树是 [i,k−1],右子树是 [k+1,j]。

根据题意"加分=左子树加分×右子树加分+根节点分数",得出状态转移方程:

dp[i][j]=max(dp[i][j],dp[i][k−1]×dp[k+1][j]+a[k])

三、 四个易错点

区间DP的代码骨架很短,但极其容易在初始化和边界上死循环。以下四个坑点,都是校队同学真实出错的地方:

  1. 空子树的合法性(越界陷阱) 当选定最左侧节点i当根时,左子树区间变为 [i,i−1]。这是一个空树。题目规定空子树加分为 1。所以必须初始化 dp[i][i−1]=1。注意,当选定最右侧节点 n 当根时,右子树变为 [n+1,n],所以初始化的循环必须开到n+1以防越界。

  2. 叶子节点的独立性(避免公式误伤) 题目规定"叶子的加分就是叶节点本身的分数"。如果我们让长度len=1的区间也进入转移方程,就会多乘上两个空子树的1,导致分数计算错误。最稳妥的做法是:手动初始化长度为1的区间(dp[i][i]=a[i]且root[i][i]=i),主循环从len=2 开始。

  3. 右端点的当根权 枚举根节点k时,必须是 for(int k=i;k<=j; k++),绝不能漏掉等号。二叉树允许只有左子树没有右子树的偏瘫形态,最右边的节点 j 完全有资格当树根。

  4. 整数溢出(数据类型陷阱) 题目明确说明最高加分可能不超过 4×10^9。这个数值已经超过了32位有符号整型int的极限(约 21.4 亿)。因此,dp数组必须果断开long long,否则虽然信息学奥赛一本通能过,但是实际上如果样例大一点是会出错的。

四、 完整代码

cpp 复制代码
#include <iostream>
using namespace std;
int n;
int a[35];//记录每个节点的原本分数
long long dp[35][35];//dp[i][j]代表i-j区间内最高加分
int root[35][35];//记录i和j区间取得最高分时的最优根节点

//输出前序遍历
void print(int l,int r){
    if(l>r) return;//遇到空节点就返回
    int k=root[l][r];//否则就输出根
    cout<<k<<" ";
    print(l,k-1);//递归左子树
    print(k+1,r);//递归右子树
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n+1;i++){//注意循环到n+1防止右侧空子树越界
        dp[i][i]=a[i];//叶子结点的最高加分就是自己本身的分数
        dp[i][i-1]=1;//初始化空节点加分位1
        root[i][i]=i;//初始化每个节点是自己的根,叶子节点当根的只能是它自己,防止递归死循环
    }
    for(int len=2;len<=n;len++){//区间长度从2开始遍历
        //枚举左端点
        for(int i=1;i<=n-len+1;i++){
            //右端点
            int j=i+len-1;
            for(int k=i;k<=j;k++){//根节点
            //状态转移,如果l×r+a大于之前加分,就更新加分
            //然后记录本次ij的最优根节点
                if(dp[i][k-1]*dp[k+1][j]+a[k]>dp[i][j]){
                    dp[i][j]=dp[i][k-1]*dp[k+1][j]+a[k];
                    root[i][j]=k;
                }
            }
        }
    }
    //输出最高加分
    cout<<dp[1][n]<<endl;
    //递归输出先序遍历顺序
    print(1,n);
}
相关推荐
王老师青少年编程5 分钟前
csp信奥赛C++高频考点专项训练之贪心算法 --【区间贪心】:雷达安装
c++·算法·贪心·csp·信奥赛·区间贪心·雷达安装
elseif1236 分钟前
分组背包1
c++·学习·算法
im_AMBER9 分钟前
Leetcode 160 最小覆盖子串 | 串联所有单词的子串
开发语言·javascript·数据结构·算法·leetcode
狐璃同学13 分钟前
数据结构(1)三要素
数据结构·算法
列星随旋19 分钟前
拓扑排序(Kahn算法)
算法
Hello!!!!!!26 分钟前
C++基础(六)——数组与字符串
c++·算法
山半仙xs32 分钟前
基于卡尔曼滤波的人脸跟踪
人工智能·python·算法·计算机视觉
智者知已应修善业1 小时前
【51单片机调用__TIME__无法实时时间】2023-7-10
c++·经验分享·笔记·算法·51单片机
做时间的朋友。1 小时前
算法-最大单入口空闲区域
算法
千寻girling1 小时前
机器学习 | 逻辑回归 | 尚硅谷学习
java·人工智能·python·学习·算法·机器学习·逻辑回归