最大连续子序列 及 延伸题目

最大连续子序列问题

问题定义
给定K个整数的序列{ N1, N2, …, NK },其任意连续子序列可表示为{ Ni, Ni+1, …, Nj },其中 1 <= i <= j <= K。最大连续子序列是所有连续子序列中元素和最大的一个, 例如给定序列{ -2, 11, -4, 13, -5, -2 },其最大连续子序列为{ 11, -4, 13 },最大和为20

解法1:朴素解法, 时间复杂度 O(K^2)

//假设给定序列:a1,a2,...,aK
maxsum=0; // 最大的连续子序列的和
for(int i=0; i<K; i++){
    tmpSum=0;
    for(int j=i; j<K; j++){
        tmpSum += a[j]
        if(tmpSum > maxsum){
            maxsum = tmpSum;
        }
    }
}

解法2:分治算法, 时间复杂度:O(nlogn)
对于任意一个序列{a1, a2, …,am,…. an}, ( m=(1+n)/2 ) 最大的连续子序列在该序列中的位置存在三种情况: 1. 位于中间部分的左边; 2. 位于中间部分的右边 ; 3. 左边和右边都含有最大的连续子序列的一部分, e.g. ai, …, am, …., aj.
对于情况1,2, 使用递归算法可以轻松计算出;对于情况3, 则通过求出前半部分的最大和(包含前半部分的最后一个元素)以及后半部分的最大和(包含后半部分的第一个元素)而得到,然后将这两个和在一起, 最后,三种情况中最大的结果就是要求的结果。

int MaxSubSum(const int A[], int Left, int Right)
{
  int MaxLeftSum,MaxRightSum;
  int MaxLeftBorderSum,MaxRightBorderSum;
  int LeftBorderSum,RightBorderSum;
  int mid,i;

  if(Left == Right) // 处理只有一个元素的子序列
  {
    if(A[Left] > 0)
      return A[Left];
    else // 对于小于等于0的元素, 
      return 0;
  }

  mid= (Left + Right)/2;
  // 情况1
  MaxLeftSum = MaxSubSum(A,Left,mid);
  // 情况2
  MaxRightSum = MaxSubSum(A,mid+1,Right);

  // 情况3
  MaxLeftBorderSum = 0;
  LeftBorderSum = 0;
  for(i = mid;i >= Left;i--)// 求解最大序列的左边部分
  {
    LeftBorderSum += A[i];
    if(LeftBorderSum > MaxLeftBorderSum)
      MaxLeftBorderSum = LeftBorderSum;
  }

  MaxRightBorderSum = 0;
  RightBorderSum = 0;
  for(i = mid+1;i <= Right;i++)// 求解最大序列的右边部分
  {
    RightBorderSum += A[i];
    if(RightBorderSum > MaxRightBorderSum)
      MaxRightBorderSum = RightBorderSum;
  } 

  return Max(MaxLeftSum,MaxRightSum,MaxLeftBorderSum + MaxRightBorderSum); // 返回三种情况中最大的结果
}

解法3: 动态规划 , 时间复杂度O(n)

引理1: 以负数开头的子序列不会是最大子序列。
证明:令 子序列开头的元素 ai < 0, 终结元素为aj, 则 ai+…+aj < ai+1+…+aj 显然成立。
引理2:对子序列{ai, …, aj}, 条件1:如果对x取{i~j}中的任意整数(包含i,不包含j) sum{ai, …, ax} >0, 但是,条件2:sum{ai, …, aj}<0,则以该子序列中的任何元素ap开头的以aj为终结的任意子序列的和必定小于0。
证明:已知aj<0 且由引理1知,ai > 0.
显然有 0 >= sum{ai, …, aj} >= sum{ai-1, …, aj}
反证法:假设sum{ap, …, aj}>0, 由引理2条件sum{ai, …, aj}<0 知道sum{ai, …, ap-1}<0,但该结论又违反了引理2中的条件:如果对x取{i~j}中的任意整数(包含i,不包含j) sum{ai, …, ax} >0,得证。

有引理1可知,若a[i]<0, 则应跳到a[i+1]作为子序列的开头元素(如果a[i+1]>0); 由引理2可知, 若a[i]+…+a[j]<=且满足引理2的其他条件,则应以a[j+1]作为子序列的开头元素(如果a[j+1]>0). 实质上,引理1是引理2的特例。
引理1和2可归结为该状态方程: maxsum(i)= max( maxsum(i-1)+ary(i), ary(i) ); (也可以由动态规划方法处理的准则:最优子结构”、“子问题重叠”、“边界”和“子问题独立”得到)
通过对给定序列顺序地反复运用引理1和引理2,最终可求得该序列的最大连续子序列。
代码如下:

int maxSubSeq(int[] ary){
    int maxsum=0;
    int localSum=0;
    for (int i=0; i<ary.length; ++i){
        localSum += ary[i];
        if(localSum > maxsum){
            maxsum= localSum;
        }else if (localSum < 0){ 
            localSum=0; // 不考虑 ai~aj中的元素作为子序列的开头, 其中ai>0, aj<0
        }//else  => localSum >0, 就是引理2中的条件1
    }
}

不同子序列的个数

问题定义:
对于一个序列a=a[1],a[2],…a[n]。任意子序列可表示为{aPk, aPk+1, …, aPk+m},其中 1<= Pk < Pk+1< Pk+m<=n, 对于给定的序列,求不同的子序列的个数。

思路
令 f(i) 表示 原序列a中前 i 个数中含有的不同子序列个数,则
情况1:若原序列中第i+1个数与前 i 个数都不相同,则 f(i+1) = f(i)+f(i)+1.
这是因为,第i+1个数可以和前 i 个数中构成的 f(i)个不同的子序列结合形成的f(i)个子序列,且第i+1个数自己可以构成一个子序列, 再加上原来的f(i)个子序列,就是原序列中前i+1个数中含有不同子序列的个数。
情况2: 若原序列中第i+1个数与前 i 个书中存在相同的数, 则 f(i+1)=f(i)+f(i)-f( last(a[i+1]) ), 其中 last(a[i+1])是数字a[i+1]在原序列中最后一次出现的位置。e.g. 对序列{1, 2,2, 5, 4, 2, 3}, 令i+1=6, 则a[i+1]=2, 那么last(a[i+1])=3,而不是2.
之所以减去f( last(a[i+1]) ),是因为 a[i+1] 和前 last(a[i+1])-1个数字组合形成的子序列与前last(a[i+1])个数字形成的子序列相同
本例中, a[i+1]=26 (6代表其下标), 因为前 last (a[i+1])=3个数字:1, 2, 23 组成的子序列三个:{1}, {2},{1,2}。而a[i+1] 和前 last(a[i+1])-1 (=2)个数字组合形成的子序列和这三个相同,但是题目要求的是不同的子序列。

final MaxLen = 99999;
int[] last = new int[MaxLen]; // 假设初始化为-1
int[] f = new int[MaxLen];
// Read inputs
int n=scan.nextInt(); // read the length of the seq
for(int i=1; i<=n; ++i){
    int x = scan.nextInt();
    if(last[x]!=-1){
        f[i] = f[i-1]*2 - f[last[x]];
    }else{
        f[i] = f[i-1]*2+1;
    }
    last[x]=i;
}

可作为Fibonacci数列前缀的非空子序列个数 [微软面试题]

问题定义
给定一个序列{an}, 求可作为fibonacci数列的非空子序列个数。 最后结果 mod 1,000,000,007!
输入
One line with an integer n.
Second line with n integers, indicating the sequence {an}.
For 30% of the data, n<=10.
For 60% of the data, n<=1000.
For 100% of the data, n<=1000000, 0<=ai<=100000.

fibonacci 数列:
F1 = 1, F2 = 1
Fn = Fn-1 + Fn-2, n>=3

样例输入
2 1 1 2 2 3
样例输出
7

解题思路

为区分样例中的每个元素, 假设重写样例为 21 12 13 24 25 36. 下标代表其在序列中索引号.
部分fibonacci数列: 1, 1, 2, 3, 5, 8, …..
上述例子中,符合条件的子序列:
{12},
{13},
{12 13},
{12 13 24},
{12 13 25},
{12 13 25 36},
{12 13 24 36}

首先明白子序列的含义: 子序列是指从原序列中任意选定一些项,保持其相对顺序不变,得到的序列为原序列的子序列。
也就是说,对于当前枚举的原序列第i项,可以和原序列第1~i-1项中任意一个子序列构成一个新的子序列。
本题中要求构成的序列必须满足斐波拉契数列的前缀,所以在第1到第i-1项中符合要求的子序列一定满足下面这个条件:
假设原序列中第1到第i-1项的某个子序列{s1 ,…, sm}是Fibonacci数列的前缀, 该子序列和原序列第i项构成新的新子序列如果是Fibonacci子序列,那么a[i], sm必须是Fibonacci数列的相邻项。

因此,不用考虑原序列中的非Fibonacci数。
此外,还需要标记出原序列中每一个留下来的项对应于斐波拉契数列的第几项

动态规划思想:
f(i,j) 表示原数列中前i个数中,以Fibonacci 数列中第 j项 (从下标1开始) 作为子序列结尾元素的子序列个数.

假设枚举到原序列的第 i 项,a[i] 是Fibonacci序列的第 j 项 (a[i]>1), 则
边界条件:f(i, 0)=1, f(i, j)=0; i 取 {0, 1, 2, …, n}中的值;j 取 {1, 2, …, n}中的值
递推公式:f(i, j) = f(i-1, j-1) + f(i-1, j)

原序列前 i 项中,以Fibonacci序列第j项作为结尾元素的子序列的构成方式:
1. 当原序列的第i个元素 a[i] 在其前的元素里没有出现过时: 原序列的第 i 项,与原序列中前 i-1 个元素以Fibonacci序列的第 j-1 项构成的子序列结合,因此 有 f(i-1, j-1)个是Fibonacci前缀新子序列。
2. 当原序列的第i个元素 a[i] 在其前的元素里出现过时:假设在原序列前 i-1 个元素中,出现过和 a[i] 相等的元素 a[i’],其中 i’ < i. 由于a[i]是Fibonacci序列的第 j 项,a[i’]也是Fibonacci序列的第 j 项,因此 f(i-1, j) 也表示原序列前 i 个元素以a[i]作为结束元素的符合要求的子序列的个数。如果在前i-1个元素中不存在和当前元素a[i]相等的元素,该项为0.

Fibonnaci数列中前两项具有相同值,因此需要特殊处理。
a[i] == 1时:
f(i, 1) = f(i-1, 1)
f(i, 2) = f(i-1, 2) + f(i-1, 1)

假设序列的长度为n, 则 可作为Fibonacci数列前缀的非空子序列个数: f(n,1)+…+f(n,K) , K 是最大fabonacci数在fabonacci数列中的下标 (从1开始)。【按照题目要求,记得 在计算过程中要记得MOD 100,000,007】

题目中给定原序列中数字最大为100,000,假设斐波拉契数列中小于等于100,000的有K项, (可提前计算出K=25)
则该递推公式的时间复杂度为O(KN)

时空优化方案

在上面的递推公式中,我们可以发现在转移过程中,f(i, j)中大部分数都是直接继承的f(i-1, j)。同时f(i, . )只会在枚举到第 i 项的时候更新,只会在计算第i+1项时用到。
因此,我们可以只使用一维数组存储解 f(j).

在更新f(j)时,需要注意: 当 a[i]==1时
要先更新作为斐波拉契数列第2项的1,再计算作为斐波拉契数列第1项的1。
这是因为f(1)更新后会影响到f(2)的计算,而反过来不会。

优化后,时间复杂度:O(n), 空间复杂度:O(n)

代码如下

// 优化后的算法
```java
import java.io.BufferedInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class Main {
    static final int Max=1000010;
    static int[] fibs=new int[Max];
    static Map<Integer,Integer> indFibs = new HashMap<>();
    static void init(){
        fibs[0]=fibs[1]=1;
        for(int i=2; ; ++i){
            fibs[i]=fibs[i-2]+fibs[i-1];
            indFibs.put(fibs[i], i+1);// 记录fib数在序列中的位置
            if(fibs[i]>=Max)
                break;
        }
        indFibs.put(1, 2);
    }
    static void display(int[] f){
        for(int i=0; i<f.length; ++i){
            System.out.print(f[i] +" ");
        }
        System.out.println();
    }
    static void solver(){
        init();
        int[] f = new int[26];
        f[0]=1;
        final int M = 1000000007;
        Scanner scan = new Scanner(new BufferedInputStream(System.in));
        int n = scan.nextInt();
        for(int i=0; i<n; ++i){
            int x = scan.nextInt(); 
            if(indFibs.containsKey(x)){
                int j = indFibs.get(x);
                f[j] = (f[j-1]+f[j]) % M;
                if(x==1){
                    f[1]=(f[1]+f[0]) % M;
                }
            }
        }
        scan.close();
        int ans = 0;
        for(int i=1; i<=25; ++i){
            ans = (ans+f[i])%M;
        }
        System.out.println(ans);
    }
    public static void main(String[] args) {
        solver();
    }
}

题目来源:hihoCoder

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值