日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

力扣---戳气球

發布時間:2024/4/11 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 力扣---戳气球 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

力扣—戳氣球

文章目錄

    • 力扣---戳氣球
    • 一、題目描述
    • 二、回溯思路
    • 三、動態規劃思路
    • 四、代碼

一、題目描述

有 n 個氣球,編號為0 到 n-1,每個氣球上都標有一個數字,這些數字存在數組 nums 中。

現在要求你戳破所有的氣球。每當你戳破一個氣球 i 時,你可以獲得 nums[left] * nums[i] * nums[right] 個硬幣。 這里的 left 和 right 代表和 i 相鄰的兩個氣球的序號。注意當你戳破了氣球 i 后,氣球 left 和氣球 right 就變成了相鄰的氣球。

求所能獲得硬幣的最大數量。

說明:

你可以假設 nums[-1] = nums[n] = 1,但注意它們不是真實存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:

輸入: [3,1,5,8] 輸出: 167 解釋: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

二、回溯思路

先來順一下解決這種問題的套路:

我們多次強調過,很顯然只要涉及求最值,沒有任何奇技淫巧,一定是窮舉所有可能的結果,然后對比得出最值

所以說,只要遇到求最值的算法問題,首先要思考的就是:如何窮舉出所有可能的結果?

窮舉主要有兩種算法,就是回溯算法和動態規劃,前者就是暴力窮舉,而后者是根據狀態轉移方程推導「狀態」

如何將我們的扎氣球問題轉化成回溯算法呢?這個應該不難想到的,我們其實就是想窮舉戳氣球的順序,不同的戳氣球順序可能得到不同的分數,我們需要把所有可能的分數中最高的那個找出來,對吧。

那么,這不就是一個「全排列」問題嘛,我們已經講解過全排列算法,其實只要稍微改一下邏輯即可,偽碼思路如下:

int res = Integer.MIN_VALUE;/* 輸入一組氣球,返回戳破它們獲得的最大分數 */ int maxCoins(int[] nums) {backtrack(nums, 0);return res; } /* 回溯算法的偽碼解法 */ void backtrack(int[] nums, int socre) {if (nums 為空) {res = max(res, score);return;}for (int i = 0; i < nums.length; i++) {int point = nums[i-1] * nums[i] * nums[i+1];int temp = nums[i];// 做選擇在 nums 中刪除元素 nums[i]// 遞歸回溯backtrack(nums, score + point);// 撤銷選擇將 temp 還原到 nums[i]} }

回溯算法就是這么簡單粗暴,但是相應的,算法的效率非常低。所以回溯算法肯定是不能通過所有測試用例的。

三、動態規劃思路

這個問題中我們每戳破一個氣球nums[i]nums[i]nums[i],得到的分數和該氣球相鄰的氣球nums[i?1]nums[i-1]nums[i?1]nums[i+1]nums[i+1]nums[i+1]有相關性 的。

我們說過運用 動態規劃算法的一個重要條件:子問題必須獨立所以對于這個戳氣球問題,如果想用動態規劃,必須巧妙地定義dp數組的含義,避免子問題產生相關性,才能推出合理的狀態轉移方程。

如何定義dp數組呢,這里需要對問題進行一個簡單地轉化。題目說可以認為nums[?1]=nums[n]=1nums[-1] = nums[n] = 1nums[?1]=nums[n]=1那么我們先直接把這兩個邊界加進去,形成一個新的數組points:

int maxCoins(int[] nums) {int n = nums.length;// 兩端加入兩個虛擬氣球int[] points = new int[n + 2];points[0] = points[n + 1] = 1;for (int i = 1; i <= n; i++) {points[i] = nums[i - 1];}// ... }

現在氣球的索引變成了從1到n,points[0]和points[n+1]points[0]和points[n+1]points[0]points[n+1]可以認為是兩個「虛擬氣球」。

那么我們可以改變問題:在一排氣球points中,請你戳破氣球0和氣球n+1之間的所有氣球(不包括0和n+1),使得最終只剩下氣球0和氣球n+1兩個氣球,最多能夠得到多少分?

現在可以定義dp數組的含義:

dp[i][j] = x表示,戳破氣球i和氣球j之間(開區間,不包括i和j)的所有氣球,可以獲得的最高分數為x。

那么根據這個定義,題目要求的結果就是dp[0][n+1]dp[0][n+1]dp[0][n+1]的值,而 base case 就是dp[i][j]=0dp[i][j] = 0dp[i][j]=0,其中0<=i<=n+1,j<=i+10 <= i <= n+1, j <= i+10<=i<=n+1,j<=i+1因為這種情況下,開區間(i, j)中間根本沒有氣球可以戳。

// base case 已經都被初始化為 0 int[][] dp = new int[n + 2][n + 2];

現在我們要根據這個dp數組來推導狀態轉移方程了,所謂的推導「狀態轉移方程」,實際上就是在思考怎么「做選擇」,也就是這道題目最有技巧的部分:

不就是想求戳破氣球i和氣球j之間的最高分數嗎,如果「正向思考」,就只能寫出前文的回溯算法;我們需要「反向思考」,想一想氣球i和氣球j之間最后一個被戳破的氣球可能是哪一個?

其實氣球i和氣球j之間的所有氣球都可能是最后被戳破的那一個,不防假設為k。回顧動態規劃的套路,這里其實已經找到了「狀態」和「選擇」:i和j就是兩個「狀態」,最后戳破的那個氣球k就是「選擇」。

根據剛才對dp數組的定義,如果最后一個戳破氣球k,dp[i][j]dp[i][j]dp[i][j]的值應該為:

dp[i][j] = dp[i][k] + dp[k][j] + points[i]*points[k]*points[j]

你不是要最后戳破氣球k嗎?那得先把開區間(i, k)的氣球都戳破,再把開區間(k, j)的氣球都戳破;最后剩下的氣球k,相鄰的就是氣球i和氣球j,這時候戳破k的話得到的分數就是points[i]*points[k]*points[j]。

那么戳破開區間(i, k)和開區間(k, j)的氣球最多能得到的分數是多少呢?嘿嘿,就是dp[i][k]和dp[k][j]dp[i][k]和dp[k][j]dp[i][k]dp[k][j],這恰好就是我們對dp數組的定義嘛!

結合這個圖,就能體會出dp數組定義的巧妙了。由于是開區間,dp[i][k]和dp[k][j]不會影響氣球k;而戳破氣球k時,旁邊相鄰的就是氣球i和氣球j了,最后還會剩下氣球i和氣球j,這也恰好滿足了dp數組開區間的定義。

那么,對于一組給定的i和j,我們只要窮舉i < k < j的所有氣球k,選擇得分最高的作為dp[i][j]dp[i][j]dp[i][j]的值即可,這也就是狀態轉移方程:

// 最后戳破的氣球是哪個? for (int k = i + 1; k < j; k++) {// 擇優做選擇,使得 dp[i][j] 最大dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + points[i]*points[j]*points[k]); }

寫出狀態轉移方程就完成這道題的一大半了,但是還有問題:對于k的窮舉僅僅是在做「選擇」,但是應該如何窮舉「狀態」i和j呢?

for (int i = ...; ; )for (int j = ...; ; )for (int k = i + 1; k < j; k++) {dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + points[i]*points[j]*points[k]); return dp[0][n+1];

四、代碼

關于「狀態」的窮舉,最重要的一點就是:狀態轉移所依賴的狀態必須被提前計算出來。

拿這道題舉例,dp[i][j]dp[i][j]dp[i][j]所依賴的狀態是dp[i][k]和dp[k][j]dp[i][k]和dp[k][j]dp[i][k]dp[k][j],那么我們必須保證:在計算dp[i][j]dp[i][j]dp[i][j]時,dp[i][k]和dp[k][j]dp[i][k]和dp[k][j]dp[i][k]dp[k][j]已經被計算出來了(其中i<k<ji < k < ji<k<j)。

那么應該如何安排i和j的遍歷順序,來提供上述的保證呢?我們\處理這種問題的一個雞賊技巧:根據 base case 和最終狀態進行推導。

PS:最終狀態就是指題目要求的結果,對于這道題目也就是dp[0][n+1]dp[0][n+1]dp[0][n+1]

我們先把 base case 和最終的狀態在 DP table 上畫出來:

對于任一dp[i][j],我們希望所有dp[i][k]和dp[k][j]已經被計算,畫在圖上就是這種情況:

那么,為了達到這個要求,可以有兩種遍歷方法,要么斜著遍歷,要么從下到上從左到右遍歷


斜著遍歷有一點難寫,所以一般我們就從下往上遍歷,下面看完整代碼:

int maxCoins(int[] nums) {int n = nums.length;// 添加兩側的虛擬氣球int[] points = new int[n + 2];points[0] = points[n + 1] = 1;for (int i = 1; i <= n; i++) {points[i] = nums[i - 1];}// base case 已經都被初始化為 0int[][] dp = new int[n + 2][n + 2];// 開始狀態轉移// i 應該從下往上for (int i = n; i >= 0; i--) {// j 應該從左往右for (int j = i + 1; j < n + 2; j++) {// 最后戳破的氣球是哪個?for (int k = i + 1; k < j; k++) {// 擇優做選擇dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + points[i]*points[j]*points[k]);}}}return dp[0][n + 1]; }

至此,這道題目就完全解決了,十分巧妙,但也不是那么難,對吧?

關鍵在于dp數組的定義,需要避免子問題互相影響,所以我們反向思考,將dp[i][j]的定義設為開區間,考慮最后戳破的氣球是哪一個,以此構建了狀態轉移方程

對于如何窮舉「狀態」,我們使用了小技巧,通過 base case 和最終狀態推導出i,j的遍歷方向,保證正確的狀態轉移。

總結

以上是生活随笔為你收集整理的力扣---戳气球的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。