鸡蛋篮子与格子取数

1.鸡蛋篮子

  有N个鸡蛋和M个篮子,把鸡蛋放到M个篮子里,每个篮子都不能为空。另外,需要满足:任意一个小于N的正整数,都能由某几个篮子内蛋的数量相加的和得到。写出程序,使得输入一个(N,M),输出所有可能的分配情况。

  思路:

  1.规定篮子鸡蛋数量从小到大递增枚举。

  basket[M]:M个篮子中的鸡蛋数量

  current_sum:当前所有篮子鸡蛋的总和,

  basket_id:当前篮子的序号,
  current_num:将要放到当前篮子去的鸡蛋数量,

  2.由题意:任意一个小于N的正整数,都能由某几个篮子内蛋的数量相加的和得到

  对于这M个篮子中的鸡蛋数量,我们用数组basket[M]来表示,所以:

  对于前n个篮子,其鸡蛋数量总和为Sn,那么对于第n+1个篮子,其鸡蛋数量应该满足:
     basket[n+1] <= Sn + 1,如果basket[n+1] > Sn + 1,那么Sn + 1这个数将无法通过相应的篮子鸡蛋数相加来获得。
    由于是非递减序列,因而 子问题之间关系
     basket[n] <= basket[n+1] <= Sn + 1

  3.剪枝要求:猜出最大 和 最小 ,其余不符合的剪掉

  最小:(current_sum + current_num*(M - basket_id)) > N

  因为是非递减序列,所以假设以后篮子全是当前篮子的个数,就是最小的情况,如果这样加起来也大于N,那么肯定不符。

  最大:(current_sum + 1)*((1<<(M - basket_id)) - 1) < N

  假设前面的篮子总和为n,那么紧挨着的后一个篮子里鸡蛋数量最大值为n+1,其后的一个篮子最大值为n + (n + 1) + 1 = 2n + 2,这之后的一个篮子的最大值为n + (n + 1) + (2n + 2) + 1 = 4n + 4......(即这里取的都是S+ 1)
  依次类推,我们发现n + 1 + (2n + 2) + (4n + 4) + ...... = (2^count - 1)*(n + 1),count表示相应的篮子数量。

  (由子问题之间关系归纳总结出整体规律

N eggs M baskets
#include <stdio.h>
#include <stdlib.h>

void solve(int current_sum, int basket_id, int current_num, int* basket, int N, int M)
{
    if (current_sum == N && basket_id == M)
    {
        int i;
        for (i = 0; i < M; i++)
            printf("%d	", basket[i]);
        printf("
");
        return;
    }

    if (current_num > N || basket_id >= M)
        return;

    if ((current_sum + current_num*(M - basket_id)) > N || 
        (current_sum + (current_sum + 1)*((1<<(M - basket_id)) - 1)) < N)
        return;

    int j;
    for (j = current_num; j <= current_sum + 1; j++)
    {
        basket[basket_id] = j;
        solve(current_sum + j, basket_id + 1, j, basket, N, M);
    }
}

int main()
{
    int N;//the number of eggs
    int M;//the number of baskets
    while (scanf("%d%d", &N, &M) != EOF)
    {
        if (N < M || N >= 1<<M || M <= 0)
            printf("Wrong data!
");
        else
            printf("The combinations are as below:
");

        int* basket = (int*)malloc(sizeof(int)*M);
        solve(0, 0, 1, basket, N, M);
        free(basket);
    }
    return 0;
}

  2.格子取数问题

  题目详情:有n*n个格子,每个格子里有正数或者0,从最左上角往最右下角走,只能向下和向右,一共走两次(即从左上角走到右下角走两趟),把所有经过的格子的数加起来,求最大值SUM,且两次如果经过同一个格子,则最后总和SUM中该格子的计数只加一次。

  1) 贪心算法

  最初想到的思路可能是让每一次的路径都是最优的,即不顾全局,只看局部,让第一次和第二次的路径都是最优。

  但问题马上就来了,虽然这一算法保证了连续的两次走法都是最优的,但却不能保证总体最优,相应的反例也不难给出,请看下图:

  

  也就是说,上面图二中的走法太追求每一次最优,所以第一次最优,导致第二次将是很差;而图三第一次虽然不是最优,但保证了第二次不差,所以图三的结果优于图二。由此可知不要只顾局部而贪图一时最优,而丧失了全局最优。

  2)动态规划

  用DP[s,i,j]来记录2次所走的状态获得的最大值,其中s表示走s步,i和j分别表示在s步后第1趟走的位置和第2趟走的位置。

  0  1  2  3  4
  1  2  3  4  5
  2  3  4  5  6
  3  4  5  6  7
  4  5  6  7  8

  上述图中,数字代表的就是步数(第二次转换为从左上向右下走)

  0  0  0  0  0
  1  1  1  1  1
  2  2  2  2  2
  3  3  3  3  3
  4  4  4  4  4

  其中步数相同的时候,i,j的取值来自此图。

  由于走过的路只加一次值,所以DP[s,i,j]中,当i=j时,sum只加一次!

  W[s,i]表示经过s步后,处于i位置,位置i对应的方格中的数字。

  综上所述,递推公式为:

  if(i != j)
    DP[s, i ,j] = Max(DP[s - 1, i - 1, j - 1], DP[s - 1, i - 1, j], DP[s - 1, i, j - 1], DP[s - 1, i, j]) + W[s,i] + W[s,j]
  else
    DP[s, i ,j] = Max(DP[s - 1, i - 1, j - 1], DP[s - 1, i - 1, j], DP[s - 1, i, j]) + W[s,i]

  

//为了便于实现,我们认为所有不能达到的状态的得分都是负无穷,参考代码如下:
//copyright@caopengcs 2013
const int N = 202;
const int inf = 1000000000;  //无穷大
int dp[N * 2][N][N];  
bool isValid(int step,int x1,int x2,int n) { //判断状态是否合法
    int y1 = step - x1, y2 = step - x2;
    return ((x1 >= 0) && (x1 < n) && (x2 >= 0) && (x2 < n) && (y1 >= 0)                         
        && (y1 < n) && (y2 >= 0) && (y2 < n));
}

int getValue(int step, int x1,int x2,int n) {  //处理越界 不存在的位置 给负无穷的值
    return isValid(step, x1, x2, n)?dp[step][x1][x2]:(-inf);
}

//状态表示dp[step][i][j] 并且i <= j, 第step步  两个人分别在第i行和第j行的最大得分 时间复杂度O(n^3) 空间复杂度O(n^3) 
int getAnswer(int a[N][N],int n) {
    int P = n * 2 - 2; //最终的步数
    int i,j,step;
    
    //不能到达的位置 设置为负无穷大
    for (i = 0; i < n; ++i) {
        for (j = i; j < n; ++j) {
            dp[0][i][j] = -inf;
        }
    
    }
    dp[0][0][0] = a[0][0];//初始值

    for (step = 1; step <= P; ++step) {
        for (i = 0; i < n; ++i) {
            for (j = i; j < n; ++j) {
                dp[step][i][j] = -inf;
                if (!isValid(step, i, j, n)) { //非法位置
                    continue;
                }
                //对于合法的位置进行dp
                if (i != j) {
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j - 1, n));
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j, n));
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i, j - 1, n));
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i, j,n));
                    dp[step][i][j] += a[i][step - i] + a[j][step - j];  //不在同一个格子,加两个数
                }
                else {
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j - 1, n));
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i - 1, j,  n));
                    dp[step][i][j] = max(dp[step][i][j], getValue(step - 1, i, j,  n));
                    dp[step][i][j] += a[i][step - i]; // 在同一个格子里,只能加一次
                }
                
            }
        }
    }
    return dp[P][n - 1][n- 1];
}    

  3)动态规划 改进

  上述实现的代码的复杂度空间复杂度是O(n^3),事实上,空间上可以利用滚动数组优化,由于每一步的递推只跟上1步的情况有关,因此可以循环利用数组,将空间复杂度降为O(n^2)。

  即我们在推算dp[step]的时候,只依靠它上一次的状态dp[step - 1],所以dp数组的第一维,我们只开到2就可以了。即step为奇数时,我们用dp[1][i][j]表示状态,step为偶数我们用dp[0][i][j]表示状态,这样我们只需要O(n^2)的空间,这就是滚动数组的方法。

参考:http://www.cnblogs.com/shuaiwhu/archive/2012/06/20/2555541.html

http://www.cnblogs.com/v-July-v/p/3320870.html

原文地址:https://www.cnblogs.com/jslee/p/3431791.html