1. 程式人生 > 實用技巧 >洛谷P1040-加分二叉樹(區間DP+題目隱含條件)

洛谷P1040-加分二叉樹(區間DP+題目隱含條件)

題目連線:https://www.luogu.com.cn/problem/P1040
CSDN食用連線:https://blog.csdn.net/qq_43906000/article/details/109208486

題目描述

設一個 n 個節點的二叉樹 \(\text{tree}\)的中序遍歷為\((1,2,3,\ldots,n)\),其中數字 \(1,2,3,\ldots,n\)為節點編號。每個節點都有一個分數(均為正整數),記第\(i\)個節點的分數為 \(d_i\)\(\text{tree}\)及它的每個子樹都有一個加分,任一棵子樹 \(\text{subtree}\)(也包含\(\text{tree}\)本身)的加分計算方法如下:

\(\text{subtree}\)的左子樹的加分 \(\times \text{subtree}\) 的右子樹的加分 + \(\text{subtree}\)的根的分數。

若某個子樹為空,規定其加分為 1,葉子的加分就是葉節點本身的分數。不考慮它的空子樹。

試求一棵符合中序遍歷為 \((1,2,3,\ldots,n)\)且加分最高的二叉樹 \(\text{tree}\)。要求輸出

\(\text{tree}\)的最高加分。

\(\text{tree}\)的前序遍歷。

輸入格式

第 1 行 1 個整數 n,為節點個數。

第 2 行 n 個用空格隔開的整數,為每個節點的分數

輸出格式

第 1 行 1 個整數,為最高加分\((Ans \le 4,000,000,000)\)

第 2 行 n 個用空格隔開的整數,為該樹的前序遍歷。

輸入輸出

輸入
5
5 7 1 2 10
輸出
145
3 1 2 4 5

說明/提示
n< 30
分數 <100

emmm,如果知道這是個區間DP的話,那麼久挺好做的,我們只需要對每個區間列舉其根放的最優位置就好了,那麼久很容易得到方程:
\(dp[i][j]=max(dp[i][j],dp[i][k-1]*dp[k+1][j]+pot[k])\)
其中k為其根的位置,也就是區間DP分割的位置。實際上當我們看到上面那個計算公式的時候也應該反應過來是個區間DP,它以根為分割點,然後左右合併的。
那麼最後的答案也就出來了,就是\(dp[1][n]\)

當然,這題如果就這樣的話那就很簡單了,接下來比較難操作的就是記錄其前序遍歷,前序遍歷是根左右,那麼我們上面不是已經確定了每個區間的根了嗎,那麼我們將每個區間的根保留下來最後來個遞迴輸出就完事了。

那麼可以得到理論AC程式碼:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int mac=100;

ll pot[mac],dp[mac][mac];
int root[mac][mac];

void print_pre(int l,int r)
{
	if (!root[l][r]) return;
	printf ("%d ",root[l][r]);
	if (l>=r) return;
	print_pre(l,root[l][r]-1); print_pre(root[l][r]+1,r);
}

int main(int argc, char const *argv[])
{
	int n;
	scanf ("%d",&n);
	for (int i=1; i<=n; i++) scanf ("%lld",&pot[i]);
	for (int i=1; i<=n; i++)
		for (int j=1; j<=n; j++) dp[i][j]=1;
	for (int i=1; i<=n; i++) dp[i][i]=pot[i],root[i][i]=i;
	for (int len=2; len<=n; len++){
		for (int i=1; i+len-1<=n; i++){
			int j=i+len-1,rt=0;
			for (int k=i; k<=j; k++){
				if (dp[i][k-1]*dp[k+1][j]+pot[k]>dp[i][j])
					rt=k,dp[i][j]=dp[i][k-1]*dp[k+1][j]+pot[k];
			}
			root[i][j]=rt;
		}
	}
	printf ("%lld\n",dp[1][n]);
	print_pre(1,n);
	return 0;
}

但實際上我們可以發現。。。。。這題它沒有SPJ,那麼我們只能看著他的樣例來一手盲猜其隱含條件是:輸出字典序最小的先序遍歷。

然後我們只需要在轉移條件相等的時候取一個最小的根就可以了。。。。然後發現還是沒有變化,於是我們似乎發現了點小問題,\(dp[i][k-1]\)它越界了!!!\(k-1\)會跑到0去,但我們的dp的關於0這一層並沒有初始化,所以。。。。。初始化之後也就不需要什麼花裡胡哨的了,就可以直接AC了。

以下是AC程式碼:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int mac=100;

ll pot[mac],dp[mac][mac];
int root[mac][mac];

void print_pre(int l,int r)
{
	if (!root[l][r]) return;
	printf ("%d ",root[l][r]);
	if (l>=r) return;
	print_pre(l,root[l][r]-1); print_pre(root[l][r]+1,r);
}

int main(int argc, char const *argv[])
{
	int n;
	scanf ("%d",&n);
	for (int i=1; i<=n; i++) scanf ("%lld",&pot[i]);
	for (int i=0; i<=n; i++)
		for (int j=0; j<=n; j++) dp[i][j]=1;
	for (int i=1; i<=n; i++) dp[i][i]=pot[i],root[i][i]=i;
	for (int len=2; len<=n; len++){
		for (int i=1; i+len-1<=n; i++){
			int j=i+len-1,rt=99999;
			for (int k=i; k<=j; k++){
				if (dp[i][k-1]*dp[k+1][j]+pot[k]>dp[i][j])
					rt=k,dp[i][j]=dp[i][k-1]*dp[k+1][j]+pot[k];
			}
			root[i][j]=rt;
		}
	}
	printf ("%lld\n",dp[1][n]);
	print_pre(1,n);
	return 0;
}