如何在 O(nlogn) 中找到总和最接近零或某个值 t 的子数组
Posted
技术标签:
【中文标题】如何在 O(nlogn) 中找到总和最接近零或某个值 t 的子数组【英文标题】:How to find the subarray that has sum closest to zero or a certain value t in O(nlogn) 【发布时间】:2013-04-29 15:05:51 【问题描述】:实际上是 Programming Pearls 2nd edition 第 8 章的第 10 题。它提出了两个问题:给定一个整数数组 A[](正数和非正数),如何找到 A[] 的和最接近 0 的连续子数组?还是最接近某个值 t?
我可以想办法解决最接近0的问题。计算前缀和数组S[],其中S[i] = A[0]+A[1]+...+A[i] .然后根据元素值对这个 S 进行排序,连同它保留的原始索引信息,找到最接近 0 的子数组和,只需迭代 S 数组并做两个相邻值的 diff 并更新最小绝对 diff。
问题是,解决第二个问题的最佳方法是什么?最接近某个值 t?任何人都可以给出代码或至少一个算法吗? (如果有人对最接近于零的问题有更好的解决方案,也欢迎回答)
【问题讨论】:
我有一个排序数组,其中的条目颜色为红色和黑色。如何找到最近的红黑对?这如何解决您的问题? 这里的“子数组”是指连续的数组元素还是可以留下孔? @MvG:我手边没有 Bentley 的副本,但我很确定他指的是连续元素。 @DavidEisenstat 我没有得到提示...排序后的数组不只包含 2 个不同的值,那么这有什么帮助? @DavidEisenstat 更详细的描述表示赞赏。 【参考方案1】:您对 0 案例的解决方案对我来说似乎没问题。这是我对第二种情况的解决方案:
您再次计算前缀总和并排序。 您将索引start
初始化为 0(排序的前缀数组中的第一个索引)end
到 last
(前缀数组的最后一个索引)
你开始迭代start
0...last
并为每个你找到对应的end
- 最后一个索引,其中前缀总和是prefix[start]
+ prefix[end]
> t
.当您发现end
时,start
的最佳解决方案是prefix[start]
+ prefix[end]
或prefix[start]
+ prefix[end - 1]
(仅当end
> 0 时才采用后者)
最重要的是,您不要从头开始为每个 start
搜索 end
- 当迭代 start
的所有可能值时,prefix[start]
的值会增加,这意味着在每次迭代中,您只对 end 的前一个值感兴趣。
start
> end
时可以停止迭代
您会从所有start
位置获得的所有值中取其最佳值。
很容易证明,这将使整个算法的复杂度达到O(n logn)
。
【讨论】:
由于总复杂度是O(n*log(n))
,您也可以使用二分搜索来查找end
的特定值start
。不过,线性算法可能更容易编码:)
你能否解释一下这部分:“当你发现 end 的最佳解决方案是 prefix[start] + prefix[end] 或 prefix[start] + prefix[end - 1]”假设排序后的前缀和为 1、2、50、100、1000、10000、100000,t 为 2。我们从前缀 [0] + 前缀 [6] 开始,即 1 + 1000000 = 100001。最好的解决方案是你'告诉我这是,还是 1 + 10000?最好的解决方案不是 1 + 2 吗?
好的,我理解上面的内容,除了如果原始数组有负#,我认为它实际上不起作用。我还认为如果 t != 0 您的解决方案将失败,因为您必须考虑 2 个前缀和在原始数组中的结尾位置。因为如果t=100,那么200-100确实是100,但是100-200离100很远。t=0没关系,因为+n和-n到0的距离相等。
作为一个具体的例子,假设原始数组是:75, 25, -75, -25, 1。前2个元素的前缀和为100,所有元素的前缀和为1。假设t = 100.1,您选择 1 和 100 作为最佳前缀和对。 1 - 100 = -99,与其他候选者相差无几。
我的解决方案将与您的类似,但需要进行一些调整。所以我会保留一个 HashMap ,将每个排序的前缀总和映射到它所代表的范围的索引。然后在比较 2 个前缀总和时,首先查看索引。所以你做 PrefixSum[i] - PrefixSum[j] 其中 i 的前缀和覆盖的范围比 j 的更大。【参考方案2】:
您可以调整您的方法。假设您有一个前缀和数组S
,正如您所写的,并且已经按总和值的升序排序。关键概念不仅是检查连续的前缀和,而是使用两个指针来指示数组S
中的两个位置。用(略带 Python 风格的)伪代码编写:
left = 0 # Initialize window of length 0 ...
right = 0 # ... at the beginning of the array
best = ∞ # Keep track of best solution so far
while right < length(S): # Iterate until window reaches the end of the array
diff = S[right] - S[left]
if diff < t: # Window is getting too small
if t - diff < best: # We have a new best subarray
best = t - diff
# remember left and right as well
right = right + 1 # Make window bigger
else: # Window getting too big
if diff - t < best # We have a new best subarray
best = diff - t
# remember left and right as well
left = left + 1 # Make window smaller
复杂性受排序限制。上述搜索最多需要循环 2n=O(n) 次迭代,每次的计算时间都受一个常数限制。请注意,上面的代码是为积极的t
设计的。
代码是为S
和t
中的积极元素而设计的。如果出现任何负整数,您最终可能会遇到right
的原始索引小于left
的情况。所以你最终会得到一个子序列和-t
。您可以在if … < best
检查中检查此情况,但如果您只在那里压制此类情况,我相信您可能会遗漏一些相关情况。底线是:接受这个想法,仔细考虑,但你必须适应负数。
注意:我认为这与 Boris Strandjev 想在his solution 中表达的一般想法相同。但是,我发现该解决方案有点难以阅读和理解,因此我提供了我自己的表述。
【讨论】:
我认为这是不正确的:首先,正如您所提到的,它不处理 -ve 值。对于所有 +ve 值,您不需要预先计算和排序前缀总和。正值子问题可以用您的算法解决,修改为保持left
和right
之间的运行总和并将其与t
进行比较。
@OnurC:正确的是,对于正数组元素,没有排序前缀和的方法也可以。我相信我的方法可能更容易扩展,它也可以处理负值。但这更像是一种直觉,我还没有考虑到这一点。无论如何,虽然我的代码对于肯定的情况可能是不必要的,但我不认为它不正确。你?如果是这样,您能否提供一个中断的示例?【参考方案3】:
为了解决这个问题,你可以自己构建一个区间树, 或平衡二叉搜索树,甚至从 STL 映射中受益,在 O(nlogn) 中。
以下是使用 STL 映射,带有lower_bound()。
#include <map>
#include <iostream>
#include <algorithm>
using namespace std;
int A[] = 10,20,30,30,20,10,10,20;
// return (i, j) s.t. A[i] + ... + A[j] is nearest to value c
pair<int, int> nearest_to_c(int c, int n, int A[])
map<int, int> bst;
bst[0] = -1;
// barriers
bst[-int(1e9)] = -2;
bst[int(1e9)] = n;
int sum = 0, start, end, ret = c;
for (int i=0; i<n; ++i)
sum += A[i];
// it->first >= sum-c, and with the minimal value in bst
map<int, int>::iterator it = bst.lower_bound(sum - c);
int tmp = -(sum - c - it->first);
if (tmp < ret)
ret = tmp;
start = it->second + 1;
end = i;
--it;
// it->first < sum-c, and with the maximal value in bst
tmp = sum - c - it->first;
if (tmp < ret)
ret = tmp;
start = it->second + 1;
end = i;
bst[sum] = i;
return make_pair(start, end);
// demo
int main()
int c;
cin >> c;
pair<int, int> ans = nearest_to_c(c, 8, A);
cout << ans.first << ' ' << ans.second << endl;
return 0;
【讨论】:
这是正确的解决方案恕我直言。它需要更多的支持。基本上它会遍历数组,保持前缀和的排序历史,对于当前的sum
,找到历史上最接近sum - t
的最佳候选者。它是 O(NlogN) 并且一次通过。
演示为我返回 c=0 的随机数
为什么我们不考虑最接近(sum + c)
的候选人?【参考方案4】:
在对这个问题进行了更多思考之后,我发现@frankyym 的解决方案是正确的解决方案。我对原始解决方案进行了一些改进,这是我的代码:
#include <map>
#include <stdio.h>
#include <algorithm>
#include <limits.h>
using namespace std;
#define IDX_LOW_BOUND -2
// Return [i..j] range of A
pair<int, int> nearest_to_c(int A[], int n, int t)
map<int, int> bst;
int presum, subsum, closest, i, j, start, end;
bool unset;
map<int, int>::iterator it;
bst[0] = -1;
// Barriers. Assume that no prefix sum is equal to INT_MAX or INT_MIN.
bst[INT_MIN] = IDX_LOW_BOUND;
bst[INT_MAX] = n;
unset = true;
// This initial value is always overwritten afterwards.
closest = 0;
presum = 0;
for (i = 0; i < n; ++i)
presum += A[i];
for (it = bst.lower_bound(presum - t), j = 0; j < 2; --it, j++)
if (it->first == INT_MAX || it->first == INT_MIN)
continue;
subsum = presum - it->first;
if (unset || abs(closest - t) > abs(subsum - t))
closest = subsum;
start = it->second + 1;
end = i;
if (closest - t == 0)
goto ret;
unset = false;
bst[presum] = i;
ret:
return make_pair(start, end);
int main()
int A[] = 10, 20, 30, 30, 20, 10, 10, 20;
int t;
scanf("%d", &t);
pair<int, int> ans = nearest_to_c(A, 8, t);
printf("[%d:%d]\n", ans.first, ans.second);
return 0;
【讨论】:
【参考方案5】:附带说明:我同意此处其他线程提供的算法。最近我头顶上有另一种算法。 制作另一个 A[] 的副本,即 B[]。在B[]里面,每个元素都是A[i]-t/n,也就是说B[0]=A[0]-t/n,B[1]=A[1]-t/n ... B [n-1]=A[n-1]-t/n。那么第二个问题实际上转化为第一个问题,一旦找到了B[]最接近0的最小子数组,同时找到了A[]最接近t的子数组。 (如果 t 不能被 n 整除,这有点棘手,但是,必须选择适当的精度。运行时间也是 O(n))
【讨论】:
【参考方案6】:我认为关于最接近 0 的解决方案存在一个小错误。在最后一步,我们不仅要检查相邻元素之间的差异,还要检查彼此不靠近的元素,如果其中一个大于 0,另一个小于 0。
抱歉,我以为我应该得到这个问题的所有答案。没看到只需要一个。【讨论】:
【参考方案7】:这是java的代码实现:
public class Solution
/**
* @param nums: A list of integers
* @return: A list of integers includes the index of the first number
* and the index of the last number
*/
public ArrayList<Integer> subarraySumClosest(int[] nums)
// write your code here
int len = nums.length;
ArrayList<Integer> result = new ArrayList<Integer>();
int[] sum = new int[len];
HashMap<Integer,Integer> mapHelper = new HashMap<Integer,Integer>();
int min = Integer.MAX_VALUE;
int curr1 = 0;
int curr2 = 0;
sum[0] = nums[0];
if(nums == null || len < 2)
result.add(0);
result.add(0);
return result;
for(int i = 1;i < len;i++)
sum[i] = sum[i-1] + nums[i];
for(int i = 0;i < len;i++)
if(mapHelper.containsKey(sum[i]))
result.add(mapHelper.get(sum[i])+1);
result.add(i);
return result;
else
mapHelper.put(sum[i],i);
Arrays.sort(sum);
for(int i = 0;i < len-1;i++)
if(Math.abs(sum[i] - sum[i+1]) < min)
min = Math.abs(sum[i] - sum[i+1]);
curr1 = sum[i];
curr2 = sum[i+1];
if(mapHelper.get(curr1) < mapHelper.get(curr2))
result.add(mapHelper.get(curr1)+1);
result.add(mapHelper.get(curr2));
else
result.add(mapHelper.get(curr2)+1);
result.add(mapHelper.get(curr1));
return result;
【讨论】:
【参考方案8】:我偶然发现了这个问题。虽然已经有一段时间了,但我只是发布它。 O(nlogn) 时间,O(n) 空间算法。这是运行 Java 代码。希望这对人们有所帮助。
import java.util.*;
public class FindSubarrayClosestToZero
void findSubarrayClosestToZero(int[] A)
int curSum = 0;
List<Pair> list = new ArrayList<Pair>();
// 1. create prefix array: curSum array
for(int i = 0; i < A.length; i++)
curSum += A[i];
Pair pair = new Pair(curSum, i);
list.add(pair);
// 2. sort the prefix array by value
Collections.sort(list, valueComparator);
// printPairList(list);
System.out.println();
// 3. compute pair-wise value diff: Triple< diff, i, i+1>
List<Triple> tList = new ArrayList<Triple>();
for(int i=0; i < A.length-1; i++)
Pair p1 = list.get(i);
Pair p2 = list.get(i+1);
int valueDiff = p2.value - p1.value;
Triple Triple = new Triple(valueDiff, p1.index, p2.index);
tList.add(Triple);
// printTripleList(tList);
System.out.println();
// 4. Sort by min diff
Collections.sort(tList, valueDiffComparator);
// printTripleList(tList);
Triple res = tList.get(0);
int startIndex = Math.min(res.index1 + 1, res.index2);
int endIndex = Math.max(res.index1 + 1, res.index2);
System.out.println("\n\nThe subarray whose sum is closest to 0 is: ");
for(int i= startIndex; i<=endIndex; i++)
System.out.print(" " + A[i]);
class Pair
int value;
int index;
public Pair(int value, int index)
this.value = value;
this.index = index;
class Triple
int valueDiff;
int index1;
int index2;
public Triple(int valueDiff, int index1, int index2)
this.valueDiff = valueDiff;
this.index1 = index1;
this.index2 = index2;
public static Comparator<Pair> valueComparator = new Comparator<Pair>()
public int compare(Pair p1, Pair p2)
return p1.value - p2.value;
;
public static Comparator<Triple> valueDiffComparator = new Comparator<Triple>()
public int compare(Triple t1, Triple t2)
return t1.valueDiff - t2.valueDiff;
;
void printPairList(List<Pair> list)
for(Pair pair : list)
System.out.println("<" + pair.value + " : " + pair.index + ">");
void printTripleList(List<Triple> list)
for(Triple t : list)
System.out.println("<" + t.valueDiff + " : " + t.index1 + " , " + t.index2 + ">");
public static void main(String[] args)
int A1[] = 8, -3, 2, 1, -4, 10, -5; // -3, 2, 1
int A2[] = -3, 2, 4, -6, -8, 10, 11; // 2, 4, 6
int A3[] = 10, -2, -7; // 10, -2, -7
FindSubarrayClosestToZero f = new FindSubarrayClosestToZero();
f.findSubarrayClosestToZero(A1);
f.findSubarrayClosestToZero(A2);
f.findSubarrayClosestToZero(A3);
【讨论】:
【参考方案9】:求解时间复杂度:O(NlogN)
解空间复杂度:O(N)
[注意这个问题不能像一些人声称的那样在 O(N) 中解决]
算法:-
-
计算给定数组[第10行]的累积数组(这里,
cum[]
)
对累积数组进行排序 [第 11 行]
C[i]-C[i+1]
, $\forall$ i∈[1,n-1] (1-based index) [第 12 行] 中的答案最少]
C++ 代码:-
#include<bits/stdc++.h>
#define M 1000010
#define REP(i,n) for (int i=1;i<=n;i++)
using namespace std;
typedef long long ll;
ll a[M],n,cum[M],ans=numeric_limits<ll>::max(); //cum->cumulative array
int main()
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n; REP(i,n) cin>>a[i],cum[i]=cum[i-1]+a[i];
sort(cum+1,cum+n+1);
REP(i,n-1) ans=min(ans,cum[i+1]-cum[i]);
cout<<ans; //min +ve difference from 0 we can get
【讨论】:
【参考方案10】:我们不能使用类似于 kadane 算法的动态规划来解决这个问题。这是我对这个问题的解决方案。如果这种方法错误,请评论。
#include <bits/stdc++.h>
using namespace std;
int main()
//code
int test;
cin>>test;
while(test--)
int n;
cin>>n;
vector<int> A(n);
for(int i=0;i<n;i++)
cin>>A[i];
int closest_so_far=A[0];
int closest_end_here=A[0];
int start=0;
int end=0;
int lstart=0;
int lend=0;
for(int i=1;i<n;i++)
if(abs(A[i]-0)<abs(A[i]+closest_end_here-0))
closest_end_here=A[i]-0;
lstart=i;
lend=i;
else
closest_end_here=A[i]+closest_end_here-0;
lend=i;
if(abs(closest_end_here-0)<abs(closest_so_far-0))
closest_so_far=closest_end_here;
start=lstart;
end=lend;
for(int i=start;i<=end;i++)
cout<<A[i]<<" ";
cout<<endl;
cout<<closest_so_far<<endl;
return 0;
【讨论】:
以上是关于如何在 O(nlogn) 中找到总和最接近零或某个值 t 的子数组的主要内容,如果未能解决你的问题,请参考以下文章
Python 数据结构与算法——从某个列表中找出两个彼此最接近但不相等的数
给定一个目标总和和一组整数,找到与该目标相加的最接近的数字子集
c_cpp [阵] 3sum最近。给定n个整数的数组S,在S中找到三个整数,使得总和最接近给定数字target。 Retur