Violent recursion to dynamic programming (2)

The previous article briefly introduced how violent recursion can be transformed into dynamic programming. If repeated solutions are found in the sub-process during the violent recursion, it proves that this violent recursion can be transformed into dynamic programming.
This post will continue the practice of violent recursive transformation into dynamic programming, which is a bit difficult.

Topic
Given an integer array arr[], cards with different values ​​are arranged in a line. Player A and Player B take each card in turn. It is stipulated that player A takes it first, and player B takes it later, but each player can only take the leftmost or rightmost card each time, player A and player B are extremely smart, please return the score of the final winner.

Violent recursion
still starts with violent recursion first, one takes it first, and the other takes it later, both of them are extremely smart, and they both know how to take it to maximize their benefits.
After the first one is taken first, when you take it again, you have to choose from the array that the second hand has finished taking.
In the same way, if the second hand waits for the first hand to take it, can it be possible to pick the most profitable one from the remaining array?
Still determine the base case first:
If you take it first, the ideal state is that when the last number in the array is left, it can still be taken away by me.
If I hold it later, the saddest thing is that I can't even get the last number in the array.
The f() function in the code is to return the maximum value that can be obtained in the range of the array L~R.
The g() function represents the maximum value that can be obtained in the range of the array L ~ R.
What needs to be noted is the change of identity. If you take it first, you will become the second hand when you take it again. When you take it second, although I am the second hand, I still choose the one with the greatest benefit from the array. It is also not good to leave it to the person who takes it first, so I will become the first person.

//先手方法
public static int f(int[] arr,int Lint R){
    
    
	//base case:先手拿,并且数组中剩一个元素,我拿走
	if(L == R){
    
    
		return arr[L];
	}
	//因为可以选择从左边拿和右边拿,从左边拿下一次就是L + 1开始,右边拿就是 R - 1 开始。
	//需要注意的是我从左或者从右拿完之后,再拿就是拿别人拿剩下的了,要以后手姿态获取其余分数,所以要调用g()方法
	int p1 = arr[L] + g(arr,L + 1,R);
	int p2 = arr[R] + g(arr, L, R -1);
	
	//两种决策中取最大值
	return Math.max(p1,p2);
}
//后手方法
public static int g(int[] arr,int L,int R){
    
    
	//剩最后一个也不是我的,毛都拿不到,return 0
	if(L == R){
    
    
		return 0;
	}
	//后手方法是在先手方法后,挑选最大值,那如果先手方法选择了L,则我要从L + 1位置选,
	//如果先手选择了R,那我要从R - 1位置开始往下选。
	//是从对手选择后再次选择最大值
	int p1 = f(arr,L + 1,R);
	int p2 = f(arr,L,R - 1);
	//因为是后手,是在先手后做决定,是被迫的,所以取Min。
	return Math.min(p1,p2);
}

The method of first hand and second hand has been determined, let’s see how to call the main process

public static int win1(int[] arr){
    
    
	//如果是无效数组,则返回一个无效数字 -1 
	if(arr == null || arr.length == 0){
    
    
		return -1;
	}
	int first = f(arr, 0 ,arr.length - 1);
	int second = g(arr,0,arr.length - 1);
	
	return Math.max(first,second);
}

The analysis and code of violent recursion have been done. Next, we will realize the first step of optimization by analyzing the calling process of violent recursion, find its dependencies, and find its repeated solutions.
To give a specific example, arr[] ranges from 0 to 7. According to the code logic of violent recursion above, let's take a look at its dependencies and calling process. If the variable parameters and dependencies are determined, can we try to optimize it into dynamic programming?
insert image description here
According to the code logic, either L + 1 on the left or R - 1 on the right can be used, so it can be determined that the variable parameters are L and R, and there will be repeated solutions throughout the process.
However, the difference is that this is a double-layer recursive circular dependency call, so if the cache table is constructed according to the variable parameter parameters L and R, two different cache tables are required to record separately.

Optimization
The entire violent recursive calling process has been analyzed before, and repeated solutions have been found, in which the variable parameters are L, R, and the cache table is built according to L, R, because it is a circular dependent call of f() and g(), so Two cache tables need to be prepared.

public static int win2(int[] arr) {
    
    
        if (arr == null || arr.length == 0) {
    
    
            return -1;
        }
        int N = arr.length;
        int[][] fmap = new int[N][N];
        int[][] gmap = new int[N][N];

        for (int i = 0; i < N; i++) {
    
    
            for (int j = 0; j < N; j++) {
    
    
                fmap[i][j] = -1;
                gmap[i][j] = -1;
            }
        }
        int first = f1(arr, 0, arr.length - 1, fmap, gmap);
        int second = g1(arr, 0, arr.length - 1, fmap, gmap);
        return Math.max(first, second);
    }

    public static int f1(int[] arr, int L, int R, int[][] fmap, int[][] gmap) {
    
    
    	// != -1,说明之前计算过该值,直接返回即可
        if (fmap[L][R] != -1) {
    
    
            return fmap[L][R];
        }
        int ans = 0;
        if (L == R){
    
    
            ans = arr[L];
        }else{
    
    
            int p1 = arr[L] + g1(arr, L + 1, R, fmap, gmap);
            int p2 = arr[R] + g1(arr, L, R - 1, fmap, gmap);

            ans = Math.max(p1, p2);
        }
        //这一步能够取得的最大值
        fmap[L][R] = ans;
        return ans;
    }


    public static int g1(int[] arr, int L, int R, int[][] fmap, int[][] gmap) {
    
    
        if (gmap[L][R] != -1){
    
    
            return gmap[L][R];
        }
        //因为如果 L == R,后手方法会返回0,默认ans也是等于0,省略一步判断
        int ans = 0;
        if (L != R){
    
    
            int p1 = f1(arr,L + 1,R,fmap,gmap);
            int p2 = f1(arr,L,R - 1,fmap,gmap);
            ans = Math.min(p1,p2);
        }

        gmap[L][R] = ans;
        return ans;
    }

Secondary optimization
We have created the cache table above and found the variables L and R. Now we might as well give an example and draw the cache table to see the corresponding relationship of each column in the table. If we can find this cache The corresponding relationship of the table, is it possible to directly obtain the maximum value of the winner after the table is constructed?
insert image description here
Array arr = {7,4,16,15,1} Because there are two cache tables, it is necessary to find out the dependencies of the two tables. Next, go back to the original violent recursive method, and find out the dependencies step by step according to the code logic.

public static int win1(int[] arr) {
    
    
        if (arr == null || arr.length == 0) {
    
    
            return -1;
        }
        int first = f(arr, 0, arr.length - 1);
        int second = g(arr, 0, arr.length - 1);
        return Math.max(first, second);
    }

    public static int f(int[] arr, int L, int R) {
    
    
        if (L == R) {
    
    
            return arr[L];
        }
        int p1 = arr[L] + g(arr, L + 1, R);
        int p2 = arr[R] + g(arr, L, R - 1);
        return Math.max(p1, p2);
    }

    public static int g(int[] arr, int L, int R) {
    
    
        if (L == R) {
    
    
            return 0;
        }
        int p1 = f(arr, L + 1, R);
        int p2 = f(arr, L, R - 1);
        return Math.min(p1, p2);
    }

From the base case of the first method f() and the second method g(), it can be seen that if L == R, the f() method is equal to the value of the array arr[L] itself at this time, and g() is 0, and because every time I only choose L or only R, it will return when L = R, so my L will never be > R. The range of L ~ R we require is the value of the entire array 0 ~ 4, at this time the graph can be filled like this.
insert image description here
Let's continue to look down. If LR gives a value randomly at this time, for example, L = 1 and R = 3 in the current fmap, let's continue to look at its dependency process.
insert image description here
According to the code, it can be seen that it depends on L + 1 and R - 1 in the g() method, so the corresponding dependency in gmap is the part marked by the circle. Correspondingly, L = 1 R = 3 also depends on the position corresponding to fmap in gmap.
insert image description here
So now there is the dependency of each position in the cache table, as well as the values ​​of fmap and gmap when L == R, can we calculate the values ​​in other grids?

the code

 public static int win3(int[] arr) {
    
    
        if (arr == null || arr.length == 0) {
    
    
            return -1;
        }
        int N = arr.length;
        int[][] fmap = new int[N][N];
        int[][] gmap = new int[N][N];
		//根据base  case填充fmap,gmap都是0,数组初始化值也是0,不用填充
        for (int i = 0; i < N; i++) {
    
    
            fmap[i][i] = arr[i];
        }
		//根据对角线填充,从第一列开始
        for (int startCol = 1; startCol < N; startCol++) {
    
    
            int L = 0;
            int R = startCol;
            while (R < N) {
    
    
            	//将调用的g()和f()都替换成对应的缓存表
                fmap[L][R] = Math.max(arr[L] + gmap[L + 1][R], arr[R] + gmap[L][R - 1]);
                gmap[L][R] = Math.min(fmap[L + 1][R], fmap[L][R - 1]);
                L++;
                R++;
            }
        }
        //最后从L ~ R位置,取最大值
        return Math.max(fmap[0][N -1],gmap[0][N-1]);
    }

Guess you like

Origin blog.csdn.net/weixin_43936962/article/details/132548425