1. 合唱团

有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗? 

输入描述:
每个输入包含 1 个测试用例。每个测试数据的第一行包含一个整数 n (1 <= n <= 50),表示学生的个数,接下来的一行,包含 n 个整数,按顺序表示每个学生的能力值 ai(-50 <= ai <= 50)。
接下来的一行包含两个整数,k 和 d (1 <= k <= 10, 1 <= d <= 50)。
输出描述:
输出一行表示最大的乘积。
输入例子1:
3
7 4 7
2 50
输出例子1:
49

思路:想到了用dp来解这题,但是接下来建立dp模型时却遇到了很多困难,尝试用 dp[n][k] 这样的形式来代表从n个学生中选取k个,那么其子问题便可以用 dp[]n-1[k-1] 这种形式来表示,对于第n个学生可以选择也可以不选择。结果陷入了思维困区中,最终的也只能解决60%的case。究其原因在于这题有个限制条件d,以及能力值有负数的情况,所以如何建立dp模型使得满足这些限定条件的同时问题的分析能够变得思维清晰才是关键。

看了下解析,首先明确的是可以使用dp来解决这题,因为1.求解的是最优化问题;2.可以分解为最优子结构。

问题的分解:

从n个学生中,选择k个,可以看成是:先从n个学生里选择最后1个,然后在剩下的里选择k-1个,并且让这1个和前k-1个满足约束条件

(注意和我之前思路的不同,这个是从n个学生中选取最后一个,我之前想的是对于最后一个学生(比如第n个学生)要么选择他要么不选。)

数学描述:

为了得到递推公式,需要建立数学模型。记第k个人的位置为one,则可以用f[one][k]表示从n个人中选择k个的方案。然后,它的子问题,需要从one前面的left个人里面,选择k-1个,这里left表示k-1个人中最后一个(即第k-1个)人的位置,因此,子问题可以表示成f[left][k-1].

学生能力数组记为arr[n+1],第 i 个学生的能力值为arr[i]
one表示最后一个人,其取值范围为[1,n];
left表示第k-1个人所处的位置,需要和第k个人的位置差不超过d,因此
max{k-1,one-d}<=left<=one-1

上面有点疑惑的是 max{k-1,one-d}<=left<=one-1 这里,为什么要在 k-1 和 one-d 中取较大的呢?因为当one-d是有可能小于0的,即选择第一个(或者前几个)时应该会到最左边,这时候left的位置要保证的一个条件是他左边剩下的人数还够选,比如 从6个人中选3个,如果one的位置在第3个人,那么下一个选的位置只能是第2个人,否则前面剩下的人数不够选了。换而言之就是说在选择left的位置是,需要保证 left >= k-1, 即从第1个到第left总共有left个人,这些人数最起码等于我们剩下要选的人数 k-1。并且按照这个准则,one也是要大于或等于k的。

在n和k定了之后,需要求解出n个学生选择k个能力值乘积的最大值。因为能力值有正有负,所以

当one对应的学生能力值为正时,
f[one][k] = max{f[left][k-1]*arr[i]},  (min{k-1,one-d}<=left<=one-1);
当one对应的学生能力值为负时
f[one][k] = max{g[left][k-1]*arr[i]},  (min{k-1,one-d}<=left<=one-1);

此处g[][]是存储n个选k个能力值乘积的最小值数组

代码:

import java.util.Scanner;

public class Main_jrh_AC {
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        while(sc.hasNext()) {
            //总人数
            int n = sc.nextInt();
            //学生能力值数组,第i个人直接对应arr[i]
            int[] arr = new int[n + 1];
            //初始化
            for (int i = 1; i <= n; i++) {//人直接对应坐标
                arr[i] = sc.nextInt();
            }
            //选择的学生数
            int kk = sc.nextInt();
            //间距
            int dd = sc.nextInt();

            /**
             * 递推的时候,以f[one][k]的形式表示
             * 其中:one表示最后一个人的位置,k为包括这个人,一共有k个人
             * 原问题和子问题的关系:f[one][k]=max{f[left][k-1]*arr[one],g[left][k-1]*arr[one]}
             */
            //规划数组
            long[][] f = new long[n + 1][kk + 1];//人直接对应坐标,n和kk都要+1
            long[][] g = new long[n + 1][kk + 1];
            //初始化k=1的情况
            for(int one = 1;one<=n;one++){
                f[one][1] = arr[one];
                g[one][1] = arr[one];
            }
            //自底向上递推
            for(int k=2;k<=kk;k++){
                for(int one = k;one<=n;one++){
                    //求解当one和k定的时候,最大的分割点
                    long tempmax = Long.MIN_VALUE;
                    long tempmin = Long.MAX_VALUE;
                    for(int left = Math.max(k-1,one-dd);left<=one-1;left++){
                        if(tempmax<Math.max(f[left][k-1]*arr[one],g[left][k-1]*arr[one])){
                            tempmax=Math.max(f[left][k-1]*arr[one],g[left][k-1]*arr[one]);
                        }
                        if(tempmin>Math.min(f[left][k-1]*arr[one],g[left][k-1]*arr[one])){
                            tempmin=Math.min(f[left][k-1]*arr[one],g[left][k-1]*arr[one]);
                        }
                    }
                    f[one][k] = tempmax;
                    g[one][k] = tempmin;
                }
            }
            //n选k最大的需要从最后一个最大的位置选
            long result = Long.MIN_VALUE;
            for(int one = kk;one<=n;one++){
                if(result<f[one][kk]){
                    result = f[one][kk];
                }
            }
            System.out.println(result);
        }
    }
}

复制了解析的代码,发现自己的思路还是太死了,这题虽然是dp,但是和典型的dp解法并不相同。自己在考虑这题的时候,一是没有分析出比如 left = Math.max(k-1,one-dd) 这样的边界条件,而是没有搞清楚具体怎么从低向上计算,三是拘泥于以前的dp算法,没有考虑到维护两个dp数组,并且坚定认为dp算法最后返回的就应该是 dp[n][k] 这种形式,总认为dp 数组计算完了就能直接得出最优解。而这题是 dp 计算完了后,还遍历了这个数组一遍,因为按照开始的dp定义,dp[n][k] 未必是题目要求的最优解。还有一点值学习的是上面的 “人直接对应坐标” 这个做法,自己在做这题时非得下标0开始,结果在计算时还得考虑下标和第几个人能不能对应的上,最后直接导致大脑爆炸。最后要注意的是取值范围,因为乘积可能很大,所以应该使用long类型而不是int。

 

2. 地牢逃脱

给定一个 n 行 m 列的地牢,其中 '.' 表示可以通行的位置,'X' 表示不可通行的障碍,若从 (x0 , y0 ) 位置出发,遍历这个地牢,和一般的游戏所不同的是,他每一步只能按照一些指定的步长遍历地牢,要求每一步都不可以超过地牢的边界,也不能到达障碍上。地牢的出口可能在任意某个可以通行的位置上。求在最坏情况下,需要多少步才可以离开这个地牢。 

输入描述:

每个输入包含 1 个测试用例。每个测试用例的第一行包含两个整数 n 和 m(1 <= n, m <= 50),表示地牢的长和宽。接下来的 n 行,每行 m 个字符,描述地牢,地牢将至少包含两个 '.'。
接下来的一行,包含两个整数 x0, y0,表示牛牛的出发位置(0 <= x0 < n, 0 <= y0 < m,左上角的坐标为 (0, 0),出发位置一定是 '.')。
之后的一行包含一个整数 k(0 < k <= 50)表示合法的步长数,接下来的 k 行,每行两个整数 dx, dy 表示每次可选择移动的行和列步长(-50 <= dx, dy <= 50)

输出描述:

输出一行一个数字表示最坏情况下需要多少次移动可以离开地牢,如果永远无法离开,输出 -1。
以下测试用例中,可以上下左右移动,在所有可通行的位置.上,地牢出口如果被设置在右下角,想离开需要移动的次数最多,为3次。

输入例子1:

3 3
...
...
...
0 1
4
1 0
0 1
-1 0
0 -1

输出例子1:

3

思路

这题有一点没有讲清楚,那就是题目中的移动其实是“跳跃”,就是说从1到3,不是从1走到2再走到3而是直接跳到3。想到用BFS来解题了,因为一看就是比较典型的BFS问题。但是还有一些细节上的问题可以学习下答案的代码。

import java.util.*;
 
public class Main {
    public static void main(String[] args){
          Scanner in = new Scanner(System.in);
             
            while (in.hasNext()) {
                  int x=in.nextInt();
                  int y=in.nextInt();
                   
                  char[][] points=new char[x][y];
                  int[][] tar=new int[x][y];
                  for(int i=0;i<x;i++){
                      String str=in.next();
                      points[i]=str.toCharArray();
                  }
                  int startx=in.nextInt();
                  int starty=in.nextInt();
                  int k=in.nextInt();
                  int[] stepx=new int[k];
                  int[] stepy=new int[k];
                  for(int i=0;i<k;i++){
                      stepx[i]=in.nextInt();
                      stepy[i]=in.nextInt();
                  }

          // 下面用了两个队列来分别存x轴位置和y轴位置,之前自己做的时候只用了一个队列,结果发现队列元素的类型不是很好选择 Queue
<Integer> xqueue=new LinkedList<Integer>(); Queue<Integer> yqueue=new LinkedList<Integer>(); xqueue.add(startx); yqueue.add(starty); tar[startx][starty]=1; //访问标记这块还是很有意思的,设置成整数而不是boolean型是为了后面取最大距离(步长) while(!xqueue.isEmpty()&&!yqueue.isEmpty()){ startx=xqueue.remove(); starty=yqueue.remove(); for(int i=0;i<k;i++){ if(startx+stepx[i]<x&&startx+stepx[i]>=0&&starty+stepy[i]<y&&starty+stepy[i]>=0) if(tar[startx+stepx[i]][starty+stepy[i]]==0){ if(points[startx+stepx[i]][starty+stepy[i]]=='.'){ tar[startx+stepx[i]][starty+stepy[i]]=tar[startx][starty]+1; xqueue.add(startx+stepx[i]); yqueue.add(starty+stepy[i]); } else tar[startx+stepx[i]][starty+stepy[i]]=-1; //访问点为X } } } int max=0; int getRoad=1; for(int i=0;i<x;i++) for(int j=0;j<y;j++){ if(points[i][j]=='.'&&tar[i][j]==0){ getRoad=0; //有存在没有被访问的“.”说明不能遍历完全,有些出口到不了。 } max=Math.max(max, tar[i][j]); // 如果所有的'.'都被访问了直接去步长最长的那个 } if(getRoad==0) System.out.println(-1); else System.out.println(max-1); } } }

两个值得借鉴的地方:两个队列存储以及步长矩阵,尤其是那个步长矩阵。其实对于循环处理问题的计算,全部计算好后再去分析比每次循环都去分析要容易考虑得多。

posted on 2018-07-31 16:16  f91og  阅读(378)  评论(0编辑  收藏  举报