牛客左神演算法題1.1
阿新 • • 發佈:2019-01-25
題目:有一排正數,玩家A和玩家B都可以看到。
每位玩家在拿走數字的時候,都只能從最左和最右的數中選擇一個。
玩家A先拿,玩家B再拿,兩人交替拿走所有的數字,
兩人都力爭自己拿到的數的總和比對方多。請返回最後獲勝者獲勝的最小分數 (PS:原題最後一句話是:”請返回最後獲勝者獲勝的分數”。顯然題目這樣描述是不對的,因為獲勝的分數可以有很多個,左神講解是說的是前提A和B絕對理智和聰明,所以意思應該就是隻能返回獲勝者獲勝分數差最小的獲勝方法才能體現這裡的絕對理智,否則獲勝的分數可能不止一個。)
具體有四種思路,如下個人寫出了三種,還有一種也是帶入改進的遞迴方法,直接將公式帶入即可,略過這一方法,其它三個方法講解看註釋即可。
package com.klordy.nowCoder;
public class RecursiveToDP {
//func1: 遞迴
/**
* 思路:設陣列為arr
* 事件f:A在arr[i,j]中進行選取時,A能夠獲取的最大分數
* 事件s:B在arr[i,j]中選取時arr[k] (k=i,j)時,下一個選取的對手A在剩餘陣列中選取的分數最小
* 那麼對於A的任意一次選取而言,選策略應該是:
* max( [f(i)+s[i+1,j]],[f(j)+s[i,j-1]] ),
* 這裡的原因就是雙方不論輸贏,都想盡量減少最終的分數差距,所以很顯然對於A選了某個數之後,
* 接下來B的選擇都是應該圍繞如何選擇能夠儘量減少A能夠獲取的分數
* (!!總分是固定的,A獲取的越少則B獲取的就越多,則不論輸贏最終分數都肯定會最接近)
*
* 再分析對於B想要達到事件s而言,他的選取具體策略公式是如何呢?
* B選擇了arr[i,j]中的arr[k], 餘下來的陣列中,
* 可以理解為A開始了新一輪的在arr'陣列中就相當於要達到事件f,
* 那麼B在選取arr[k]的時候,需要考慮的很顯然就是要讓A在新一輪的arr'陣列中獲取的分數是最小的
* 如果B選擇了arr[i],那麼A就是在arr'=arr[i+1,j]中選擇則最大分數的選擇f[i+1,j]
* 如果B選擇了arr[j],那麼A就是在arr'=arr[i,j-1]中選擇則最大分數的選擇f[i,j-1]
* 對於B而言它的選擇那麼很顯然就是f[i+1,j]和f[i,j-1]哪個小我就選哪個,所以時間s的具體選取策略就是:
* min( f[i+1,j]],f[i,j-1]] )
*
* 假設剛開始是A先選,則A獲取的最高分數應該是:
* max( [f(0)+s[1,n]],[f(n)+s[0,n-1]] )
* 此時B的分數則是:
* min[ s]
*
* 因此這裡我們只需要把時間f和s均實現就OK了
*
*/
private static int f(int[] arr, int i, int j){
//如果i==j說明只剩下一個數,則先選擇的人只有一個選擇為arr[i]
if(i==j){
return arr[i];
}
return Math.max(arr[i]+s(arr,i+1,j),arr[j]+s(arr,i,j-1));
}
private static int s(int[] arr, int i, int j){
//i==j說明只有一個數,那麼後選的人肯定只能得到0分
if(i==j){
return 0;
}
return Math.min(f(arr,i+1,j),f(arr,i,j-1));
}
private static int func1(int[] arr){
if(arr==null || arr.length==0){
return 0;
}
//返回A先選能夠獲取的最大分,以及B後選獲取的最大分中的較大值,就是獲勝者分數
return Math.max(f(arr,0,arr.length-1),s(arr,0,arr.length-1));
}
//fun2: convert to DP
/**
* 對於遞迴而言,如果每次遞迴所求結果只和之前的結果有關而和後續的結果無關,則可以轉換成動態規劃的方式求解
* 對於這個問題而言,假設方法一中遞迴的兩個事件分別代表連個狀態值的矩陣,f(nxn)和s(nxn)
* 那麼很顯然:
* 1>. f(i,j)=Math.max(arr[i]+s(i+1,j),arr[j]+s(i,j-1))
* 2>. s(i,j)=Math.min(f(i+1,j),f(i,j-1));
* 即這兩個f和s矩陣是相互關聯的。
* 並且對於f(i,i) (i=0,...,n-1)而言,f(i,i)=arr[i]
* 另外對於s(i,i)=0
* 因此,由公式1>和2>可以知道,在已知f和s對角線上元素的值的情況下,可以依次遞推得到矩陣f和s中的所有值
* 假設陣列為(0,1,2,3,4),那麼矩陣為f(5x5)和s(5x5),已知對角元素f(i,i)=i,s(i,i)=0 i=0,...,5
* 那麼帶入公式我們可以知道 s(2,3)=Math.min(f(3,3),f(2,2))=2,類似的可以求出各個元素的值
* 那麼最終結果就是Math.max(f(0,n-1),s(0,n-1)),以上公式1>和公式2>就是這個問題中動態規劃的公式
*/
private static int func2(int[] arr){
if(arr==null||arr.length==0){
return 0;
}
int size = arr.length;
int[][] f = new int[size][size];
int[][] s = new int[size][size]; //由於java數值初始化的時候預設各個位置數值就是0,所以s不需要給對角線元素賦值
for(int j=0;j<size;j++){
f[j][j] = arr[j];//對角元素的賦值
for(int i=j-1;i>=0;i--){
f[i][j] = Math.max(arr[i]+s[i+1][j],arr[j]+s[i][j-1]);
s[i][j] = Math.min(f[i+1][j],f[i][j-1]);
}
}
return Math.max(f[0][size-1],s[0][size-1]);
}
//fun3 優化func2的空間和時間複雜度
/**
* 在func2中,空間複雜度相當於O(nx2)
* 因為需要計算兩個nxn的f和s矩陣,然而觀察公式1>和2>可以發現,這兩個公式完全可以合二為一
* 由2>可知: s(i+1,j)=Math.min(f(i+2,j),f(i+1,j-1)) ,
* s(i,j-1)=Math.min(f(i+1,j-1),f(i,j-2))
* 帶入1>,則有:
* --> f(i,j)=Math.max(arr[i]+s(i+1,j),arr[j]+s(i,j-1))
* =Math.max( {arr[i]+Math.min(f(i+2,j),f(i+1,j-1))} , {arr[j]+Math.min(f(i+1,j-1),f(i,j-2))} )
* 由上式知道,f(i,j)的值依賴於f(i+2,j)、f(i+1,j-1)、f(i,j-2)這三者的大小關係,
* 通過畫圖可以發現,f(i,j)所依賴的這三個元素,應該是在f(i,j)所在矩形斜邊的左移兩位的斜邊上的連續三個元素。
* 對此我們初始化的時候需要知道除了對角線以外,靠近對角線的右上方斜邊上的元素值也要知道,這樣才能得到所有的矩陣f中的值。
* 得到f中所有元素的值以後,取得f(0,n-1)就是A先選的情況下,A能夠獲取的最大分數,並且已知總分數為sum(arr),記總分數為total
* 則最終結果為Math.max( f(0,n-1),(total-f(0,n-1)) )
*/
private static int func3(int[] arr){
if(arr==null||arr.length==0){
return 0;
}
if(arr.length==1){
return arr[0];
}
if(arr.length==2){
return Math.max(arr[0],arr[1]);
}
int size = arr.length;
int[][] f = new int[size][size];
int sum = 0;
for (int i : arr) {
sum+=i;
}
for(int j=0;j<size;j++){
f[j][j] = arr[j];//對角元素的賦值
}
for(int j=0;j<size-1;j++){
f[j][j+1] = Math.max(arr[j],arr[j+1]); //初始化對角線上方斜線的初始值
}
for(int j=2;j<size;j++){//對角兩個元素已經初始化過了
for(int i=j-2;i>=0;i--){//也可以是i=j,不過由於f(i,i)和f(i-1,i)應該已經初始化過,所以可以從j-2開始計算。
f[i][j] = Math.max( (arr[i] + Math.min(f[i+2][j],f[i+1][j-1])),
(arr[j] + Math.min(f[i+1][j-1],f[i][j-2])) );
}
}
return Math.max(f[0][size-1],sum-f[0][size-1]);
}
public static int[] generateRondomArray() {
int[] res = new int[(int) (Math.random() * 20) + 1];
for (int i = 0; i < res.length; i++) {
res[i] = (int) (Math.random() * 20) + 1;
}
return res;
}
public static void main(String[] args) {
int testTime = 50000;
boolean err = false;
for (int i = 0; i < testTime; i++) {
int[] arr = generateRondomArray();
int r1 = func1(arr);
int r2 = func2(arr);
int r3 = func3(arr);
if (r1 != r2 || r1 != r3) {
err = true;
}
}
if (err) {
System.out.println("2333333333");
} else {
System.out.println("6666666666");
}
}
}