Array Sum - Don't Panic! >> 关于部分数组固定和的一类问题解析


Don't Panic! » 关于部分数组固定和的一类问题解析
前面写过2sum问题,回顾一下:给定一个数组,求其中和等于给定和的两个数。思想很简单,双指针的思想:对于排序数组,一个头指针一个尾指针,不断向中间压缩。如果数组是排序的,则时间复杂度为O(n),空间复杂度为O(1)。由此可以K sum问题,如果看过《算法引论》这本书,我们可以把K sum问题归约到K-1 sum。举个例子,3 sum问题归约到2sum 问题。对于数组中的每一个数A[i],对数组A[i+1…n]进行2sum求解,则时间复杂度为O(n^2)。类似的我们知道K sum问题的时间复杂度为O(n^k-1)。然而我们今天要讨论的却是另外一类问题。

问题一,给定一个只包含正数数组,求连续子数组和等于给定目标值的区间。和2 sum问题有点相同,也是双指针或者叫快慢指针,但是这里双指针初始化都指向头部。快指针先移动,当快慢指针区间内的和小于给定值时,移动快指针;否则移动慢指针。时间复杂度O(n),值得注意的是,此时并没有数组排序好的要求。
void subArraySum(int A[]int n, int target)
{
    if(n==0)
        return;

    int head=0, tail=0, sum=A[0];

    while(tail<n){
        if(sum==target){
            printf("%d\t%d\n", head, tail);
            sum -= A[head++];
        }
        else if(sum < target){
            ++tail;
            if(tail==n)
                break;
            sum += A[tail];
        }
        else
            sum -= A[head++];
    }
}

问题二、给定数组,包含正数、负数和0。求连续子数组和为0的最大区间。仔细比较问题一和问题二,可以发现此时数组的元素属性改变了。那么对应的方法也就不好用了。仔细分析,我们很容易想到方法,整体思想是类似的。首先对于数组A[1…n]定义数组sum[1…n],sum[i]=A[0]+A[1]+…+A[i]=sum[i-1]+A[i]。所以我们可以在O(n)的时间内求得数组sum[1…n]。则很显然可以想到,对于区间[i,j],如果其和为0,则有sum[j]=sum[i-1]。具体处理则只需要将数组sum[1…n]排序并且保持原下标不变,值得注意的是,由于数组sum[1…n]的值大小上下界是确定的,所以可以采用O(n)的桶排序,最后再遍历一遍桶就OK了。这里有一点可以说的就是桶排序的应用。其实我们并不需要将sum数组排好序,只要把sum数组放到对应的桶中,就可以了,然后处理每一个桶。对于桶i中的元素j,k,意义如下:sum[j]=sum[k]=i,而且桶中的元素是递增的(这个容易理解,因为我们是按sum从前到尾遍历的)。看下代码就都明白了。
/用map实现桶排序
int BucketSort(int sum[]int n)
{
    map<int, vector<int> >bucket;

    for(int i=0; i<n; ++n)
        bucket[sum[i]].push_back(i);
    int maxInterval=0;
    for(map<int,vector<int> >::iterator iter=bucket.begin(); iter!=bucket.end(); ++iter)
    {
        int sizev = iter->second.size();
        maxInterval = max(iter->second[sizev-1]-iter->second[0], maxInterval);
    }
    return maxInterval;
}

int subArraySum0(int A[]int n)
{
    if(n==0)
        return 0;
    int *sum = new int [n];

    sum[0] = A[0];
    for(int i=1; i<n; ++i)
        sum[i] = sum[i-1]+A[i];
    int ret = BucketSort(sum, n);
    delete [] sum;
    return ret;
}

问题三、问题二的变体,追求的和不是0而是target的时候怎么办?前面的思路都是一样,后面的怎么处理呢?前面提到桶排序,则我们可以在桶上直接来求。比如对于buck[i]就是表示和为i的sum区间,则只要考查buck[i-target]和buck[i+target]就行了。处理的时候,我们可以从一个确界开始,比如下界就考察buck[i]和buck[i+target],上界就考察buck[i]和buck[i-target]。
int BucketSort(int sum[]int n, int target)
{
    map<int, vector<int> >bucket;

    for(int i=0; i<n; ++n)
        bucket[sum[i]].push_back(i);
    int maxInterval=0;
    for(map<int,vector<int> >::iterator iter=bucket.begin(); iter!=bucket.end(); ++iter)
    {
        map<int,vector<int> >::iterator pos=bucket.find(iter->first+target);
        if(pos != bucket.end())
        {
            maxInterval = max(maxInterval, pos->second.back()-iter->second.front());
        }
    }
    return maxInterval;
}

问题四、输入数组A[],包含正数、负数、0。求正数与负数个数相同的最长的子数组。如果能把这题和上面的联系起来的话,也可以很快想出解决方案。把sum数组换成diff[1…n],diff[i]表示数组A[1…i]中正数比负数多的个数。这种方法说起来也简单,而且也不难理解。再或者,我们思维偷个懒,直接把正数替换成1,负数替换成-1,那么不就变成求子数组区间为0的问题了吗?想法很简单,代码也很简单,只需要遍历一遍数组,替换,然后调用上面的subArraySumN()函数就可以。
问题五、输入数组A[],只包含0,1,求一个最长且0和1个数相等的子串。类似于问题四的替换思想,把0换成-1,又变成子数组和为0的区间问题了。
问题六、输入数组A[],只包含数字1、2、3,不进行比较,求1,2,3的个数?运用上面的替换思想,可以有个很巧妙的解法。也就是把1替换成第一个素数也就是2,2替换成第二个素数3,3替换成第3个素数5。求个积,然后不断模+除就行了。这个也很容易理解,考虑到素数的性质:只能被自身和1整除。另外,任何一个整数的乘法因子都是素数。(如果不是素数,则可以继续分解)。下面看一个替换后的处理代码,替换的代码就不上了。
void primeFunc(int A[]int n)
{
    int sum=1;
    for(int i=0; i<n; ++i)
        sum *= A[i];

    int factor[] = {2,3,5};
    int num[] = {0,0,0};
    int pos=0;
    while(sum>1 && pos<3){
        while(sum%factor[pos] == 0){
            ++num[pos];
            sum /= factor[pos];
        }
        ++pos;
    }
    for(int i=0; i<3; ++i)
        cout << num[i] << endl;
}
这里只考虑了本题的情况,所以代码中出现了很多的幻数。正常设计成这样的接口void primeFunc(int A[], int n, int m)会好很多,m表示数组里有多少个不同的素数。另外还应该考虑到数的溢出问题,对于有些语言可能就需要自己构造大数类了。

关于部分数组固定和的一类问题解析续
给定整数数组A[1…n],判断是否可以从中选出若干个数,使它们的和恰好为K。比如A[]={1,2,4,7},K=13。应该返回2,4,7。
先介绍一种简单的方法:状态搜索,对于数组A[]中的元素只有两种可能:选或者不选。所以我们可以得到下面这张图,然后对这张图进行搜索就可以了。思想很简单,代码写起来就是深度优先搜索和广度优先搜索那一套,代码实现起来也比较模式化,注意下边界条件就可以.
void DFS(vector<int> &candidates, vector<int> elements, int target, int pos)
{
    if(target == 0)
    {
        ret.push_back(elements);
        return;
    }
    if(pos==candidates.size() || target<0)
        return;

    DFS(candidates, elements, target, pos+1);

    elements.push_back(candidates[pos]);
    DFS(candidates, elements, target-candidates[pos], pos+1);
    elements.pop_back();
}
但是这个代码是不是没有问题了呢?考虑一种情况A[]={1,1,6,1}, K=8。则上面的代码返回{1,1,6}, {1,6,1}, {1,6,1},不要问为什么,人工测试一下就明白了。下面对代码进行了一点优化。现在比较流行微创新,其实一点点小的改进往往是程序中最关键的地方。
void DFS(vector<int> &candidates, vector<int> elements, int target, int pos)
{
    if(target == 0)
    {
        ret.push_back(elements);
        return;
    }
    if(pos==candidates.size() || target<0)
        return;

    int same=1;
    for(size_t i=pos+1; i<candidates.size() && candidates[pos]==candidates[i]; i++,same++)
        ;
    for(int i=0; i<same; i++)
    {
        for(int j=0; j<i; j++)
            elements.push_back(candidates[pos]);
        DFS(candidates, elements, target-candidates[pos]*i, pos+same);
        for(int j=0; j<i; j++)
            elements.pop_back();
    }
}

vector<vector<int> > combinationSum(vector<int> &candidates, int target){
    vector<int> elements;
    sort(candidates.begin(), candidates.end());

    DFS(candidates, elements, target, 0);
    return ret;
}
给定整数数组,元素各不相同,并且每个元素使用多次,其他条件和要求不变。仿照上面的代码框架很快就可以写出下面的代码来了。
void DFS(vector<int> &candidates, vector<int> elements, int target, int pos)
{
    if(target == 0)
    {
        ret.push_back(elements);
        return;
    }
    if(pos==candidates.size())
        return;
    for(int i=0; candidates[pos]*i<=target; i++)
    {
        for(int j=0; j<i; j++)
            elements.push_back(candidates[pos]);
        DFS(candidates, elements, target-candidates[pos]*i, pos+1);
        for(int j=0; j<i; j++)
            elements.pop_back();
    }
}

vector<vector<int> > combinationSum(vector<int> &candidates, int target){
    vector<int> elements;
    sort(candidates.begin(), candidates.end());

    DFS(candidates, elements, target, 0);
    return ret;
}

仔细分析一下上面的问题,如果数组大小为N,那么状态数就是2^N,复杂度高达O(2^N)。搜索我们基本可以不考虑了。这时候另外一种重要的方法开始登场了,对,就是动态规划,关于动态规划,这里就不介绍了。我们这里只针对具体的问题,从问题中分析出不变的东西。废话不多说,下面如何分析这个问题。为了方便的分析问题,集中到核心问题上来,我们不要求返回具体元素,只需要返回true或false。这里假定大家都了解了动态规划的基本的知识,如果不清楚,建议学习《算法导论》上关于动态规划的讲解。动态规划中,最重要的是最优子结构的证明和状态转移方程的推导。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。对于本题,我们作如下定义:dp[i+1][j]==(数组A[0…i]可以表示出j?)。也就是说如果能表示,则为true,否则为false。所以题目等价于求dp[n+1][K],而dp[n+1][K]的求解取决于dp[n][K]和dp[n][K-A[n]],这样就分解成子问题。状态转移方程也很简单:dp[i+1][j]=dp[i][j]|dp[i][j-A[i]]。最后还有一点要注意的是初始化!
bool combinationSum(int A[]int n, int K){
    bool **dp = new bool* [n+1];
    for(int i=0; i<=n; i++)
        dp[i] = new bool [K+1];

    //初始化
    for(int i=0; i<=n; i++)
        dp[i][0] = true;
    for(int i=0; i<=n; i++)
    {
        for(int j=1; j<=K; j++)
            dp[i][j] = false;
    }

    for(int i=1; i<=n; i++)
    {
        for(int j=1; j<=K; j++){
            dp[i][j] = j-A[i-1]>0 ? (dp[i-1][j]|dp[i-1][j-A[i-1]]) : dp[i-1][j];
        }
    }
    bool ret = dp[n][K];

    for(int i=0; i<n; i++)
        delete [] dp[i];
    delete [] dp;

    return ret;
}
Read full article from Don't Panic! » 关于部分数组固定和的一类问题解析

Labels

LeetCode (1432) GeeksforGeeks (1122) LeetCode - Review (1067) Review (882) Algorithm (668) to-do (609) Classic Algorithm (270) Google Interview (237) Classic Interview (222) Dynamic Programming (220) DP (186) Bit Algorithms (145) POJ (141) Math (137) Tree (132) LeetCode - Phone (129) EPI (122) Cracking Coding Interview (119) DFS (115) Difficult Algorithm (115) Lintcode (115) Different Solutions (110) Smart Algorithm (104) Binary Search (96) BFS (91) HackerRank (90) Binary Tree (86) Hard (79) Two Pointers (78) Stack (76) Company-Facebook (75) BST (72) Graph Algorithm (72) Time Complexity (69) Greedy Algorithm (68) Interval (63) Company - Google (62) Geometry Algorithm (61) Interview Corner (61) LeetCode - Extended (61) Union-Find (60) Trie (58) Advanced Data Structure (56) List (56) Priority Queue (53) Codility (52) ComProGuide (50) LeetCode Hard (50) Matrix (50) Bisection (48) Segment Tree (48) Sliding Window (48) USACO (46) Space Optimization (45) Company-Airbnb (41) Greedy (41) Mathematical Algorithm (41) Tree - Post-Order (41) ACM-ICPC (40) Algorithm Interview (40) Data Structure Design (40) Graph (40) Backtracking (39) Data Structure (39) Jobdu (39) Random (39) Codeforces (38) Knapsack (38) LeetCode - DP (38) Recursive Algorithm (38) String Algorithm (38) TopCoder (38) Sort (37) Introduction to Algorithms (36) Pre-Sort (36) Beauty of Programming (35) Must Known (34) Binary Search Tree (33) Follow Up (33) prismoskills (33) Palindrome (32) Permutation (31) Array (30) Google Code Jam (30) HDU (30) Array O(N) (29) Logic Thinking (29) Monotonic Stack (29) Puzzles (29) Code - Detail (27) Company-Zenefits (27) Microsoft 100 - July (27) Queue (27) Binary Indexed Trees (26) TreeMap (26) to-do-must (26) 1point3acres (25) GeeksQuiz (25) Merge Sort (25) Reverse Thinking (25) hihocoder (25) Company - LinkedIn (24) Hash (24) High Frequency (24) Summary (24) Divide and Conquer (23) Proof (23) Game Theory (22) Topological Sort (22) Lintcode - Review (21) Tree - Modification (21) Algorithm Game (20) CareerCup (20) Company - Twitter (20) DFS + Review (20) DP - Relation (20) Brain Teaser (19) DP - Tree (19) Left and Right Array (19) O(N) (19) Sweep Line (19) UVA (19) DP - Bit Masking (18) LeetCode - Thinking (18) KMP (17) LeetCode - TODO (17) Probabilities (17) Simulation (17) String Search (17) Codercareer (16) Company-Uber (16) Iterator (16) Number (16) O(1) Space (16) Shortest Path (16) itint5 (16) DFS+Cache (15) Dijkstra (15) Euclidean GCD (15) Heap (15) LeetCode - Hard (15) Majority (15) Number Theory (15) Rolling Hash (15) Tree Traversal (15) Brute Force (14) Bucket Sort (14) DP - Knapsack (14) DP - Probability (14) Difficult (14) Fast Power Algorithm (14) Pattern (14) Prefix Sum (14) TreeSet (14) Algorithm Videos (13) Amazon Interview (13) Basic Algorithm (13) Codechef (13) Combination (13) Computational Geometry (13) DP - Digit (13) LCA (13) LeetCode - DFS (13) Linked List (13) Long Increasing Sequence(LIS) (13) Math-Divisible (13) Reservoir Sampling (13) mitbbs (13) Algorithm - How To (12) Company - Microsoft (12) DP - Interval (12) DP - Multiple Relation (12) DP - Relation Optimization (12) LeetCode - Classic (12) Level Order Traversal (12) Prime (12) Pruning (12) Reconstruct Tree (12) Thinking (12) X Sum (12) AOJ (11) Bit Mask (11) Company-Snapchat (11) DP - Space Optimization (11) Dequeue (11) Graph DFS (11) MinMax (11) Miscs (11) Princeton (11) Quick Sort (11) Stack - Tree (11) 尺取法 (11) 挑战程序设计竞赛 (11) Coin Change (10) DFS+Backtracking (10) Facebook Hacker Cup (10) Fast Slow Pointers (10) HackerRank Easy (10) Interval Tree (10) Limited Range (10) Matrix - Traverse (10) Monotone Queue (10) SPOJ (10) Starting Point (10) States (10) Stock (10) Theory (10) Tutorialhorizon (10) Kadane - Extended (9) Mathblog (9) Max-Min Flow (9) Maze (9) Median (9) O(32N) (9) Quick Select (9) Stack Overflow (9) System Design (9) Tree - Conversion (9) Use XOR (9) Book Notes (8) Company-Amazon (8) DFS+BFS (8) DP - States (8) Expression (8) Longest Common Subsequence(LCS) (8) One Pass (8) Quadtrees (8) Traversal Once (8) Trie - Suffix (8) 穷竭搜索 (8) Algorithm Problem List (7) All Sub (7) Catalan Number (7) Cycle (7) DP - Cases (7) Facebook Interview (7) Fibonacci Numbers (7) Flood fill (7) Game Nim (7) Graph BFS (7) HackerRank Difficult (7) Hackerearth (7) Inversion (7) Kadane’s Algorithm (7) Manacher (7) Morris Traversal (7) Multiple Data Structures (7) Normalized Key (7) O(XN) (7) Radix Sort (7) Recursion (7) Sampling (7) Suffix Array (7) Tech-Queries (7) Tree - Serialization (7) Tree DP (7) Trie - Bit (7) 蓝桥杯 (7) Algorithm - Brain Teaser (6) BFS - Priority Queue (6) BFS - Unusual (6) Classic Data Structure Impl (6) DP - 2D (6) DP - Monotone Queue (6) DP - Unusual (6) DP-Space Optimization (6) Dutch Flag (6) How To (6) Interviewstreet (6) Knapsack - MultiplePack (6) Local MinMax (6) MST (6) Minimum Spanning Tree (6) Number - Reach (6) Parentheses (6) Pre-Sum (6) Probability (6) Programming Pearls (6) Rabin-Karp (6) Reverse (6) Scan from right (6) Schedule (6) Stream (6) Subset Sum (6) TSP (6) Xpost (6) n00tc0d3r (6) reddit (6) AI (5) Abbreviation (5) Anagram (5) Art Of Programming-July (5) Assumption (5) Bellman Ford (5) Big Data (5) Code - Solid (5) Code Kata (5) Codility-lessons (5) Coding (5) Company - WMware (5) Convex Hull (5) Crazyforcode (5) DFS - Multiple (5) DFS+DP (5) DP - Multi-Dimension (5) DP-Multiple Relation (5) Eulerian Cycle (5) Graph - Unusual (5) Graph Cycle (5) Hash Strategy (5) Immutability (5) Java (5) LogN (5) Manhattan Distance (5) Matrix Chain Multiplication (5) N Queens (5) Pre-Sort: Index (5) Quick Partition (5) Quora (5) Randomized Algorithms (5) Resources (5) Robot (5) SPFA(Shortest Path Faster Algorithm) (5) Shuffle (5) Sieve of Eratosthenes (5) Strongly Connected Components (5) Subarray Sum (5) Sudoku (5) Suffix Tree (5) Swap (5) Threaded (5) Tree - Creation (5) Warshall Floyd (5) Word Search (5) jiuzhang (5)

Popular Posts