【问题标题】:Range in range sum with point update范围总和中的范围与点更新
【发布时间】:2014-11-14 09:45:02
【问题描述】:

给定一个数组 A,其中包含 N 个元素和 N 个范围,每个范围的形式为 [L, R]。将范围的称为A中从索引L到索引R的所有元素的总和。

示例:设数组 A = [2 5 7 9 8],给定范围为 [2,4],则该范围的值为 5+7+9=21

现在我们得到了 Q 查询,每个查询都是 2 种类型之一:

1. 0 X Y : It means change Xth element of array to Y.
2. 1 A B : It means we need to report the sum of values of ranges from A to B.

示例:让数组 A = [2 3 7 8 6 5] 并让我们有 3 个范围:

R1: [1,3] Then value corresponding to this range is 2+3+7=12
R2: [4,5] Then value corresponding to this range is 8+6=14
R3: [3,6] Then value corresponding to this range is 7+8+6+5=26

现在让我们有 3 个查询:

Q1: 1 1 2
Then here answer is value of Range1 + value of Range2 = 12+14=26 

Q2: 0 2 5
It means Change 2nd element to 5 from 3.It will change the result of Range 1.
Now value of Range1 becomes 2+5+7=14

Q3: 1 1 2
Then here answer is value of Range1 + value of Range2 = 14+14=28 

如果我们有 10^5 个查询并且 N 也高达 10^5,该怎么做。如何高效地向 Queries2 报告?

我的方法:第一个查询很容易处理。我可以从数组中构建一个段树。我可以用它来计算第一个数组(第二个数组中的一个元素)中间隔的总和。但是我如何处理 O(log n) 中的第二个查询?在最坏的情况下,我更新的元素将在第二个数组的所有区间中。

我需要 O(Qlog N) 或 O(Q(logN)^2) 解决方案。

显然我们不能为每个查询都有一个 O(N)。所以请帮助获得有效的方法

我当前的代码:

#include<bits/stdc++.h>
using namespace std;
long long  arr[100002],i,n,Li[100002],Ri[100002],q,j;
long long  queries[100002][2],query_val[100002],F[100002],temp;
long long   ans[100002];
int main()
{
scanf("%lld",&n);
for(i=1;i<=n;i++)
    scanf("%lld",&arr[i]);
for(i=1;i<=n;i++)
{
    scanf("%lld%lld",&Li[i],&Ri[i]);
}
for(i=1;i<=n;i++)
{
    F[n] = 0;
    ans[i] = 0;
}
scanf("%lld",&q);
for(i=1;i<=q;i++)
{
    scanf("%lld",&query_val[i]);
    scanf("%lld%lld",&queries[i][0],&queries[i][1]);
}
for(i=1;i<=n;i++)
{
    for(j=Li[i];j<=Ri[i];j++)
    {
        F[i] = F[i] + arr[j];
    }
}
long long  diff;
long long  ans_count = 0,k=1;
for(i=1;i<=q;i++)
{
    if(query_val[i] == 1)
    {
        temp = arr[queries[i][0]];
        arr[queries[i][0]] = queries[i][1];
        diff =  arr[queries[i][0]] - temp;
        for(j=1;j<=n;j++)
        {
            if(queries[i][0]>=Li[j] && queries[i][0]<=Ri[j])
                F[j] = F[j] + diff;
            ++k;
        }

    }
    else if(query_val[i] == 2)
    {
        ++ans_count;
        for(j=queries[i][0];j<=queries[i][1];j++)
            ans[ans_count] = ans[ans_count] + F[j];

    }
}
for(i=1;i<=ans_count;i++)
{
    printf("%lld\n",ans[i]);
}
return 0;
}

虽然代码是正确的,但对于较大的测试用例需要大量时间。请帮助

【问题讨论】:

  • @j_random_hacker 怎么样?请帮忙解释一下
  • 我真的无法比 Peter Fenwick 的原始论文更好地解释它,您可以从 Wikipedia 页面找到它,这是“Fenwick 树”的第一个 Google 结果。
  • 听起来你已经有了正确的方法......每个元素最多只能出现在段树的log N间隔中。
  • @arghbleargh 你能解释一下你是怎么得到它的吗?
  • @j_random_hacker 我知道 Fenwick 树,但你是如何在这里实现它们的?

标签: c++ algorithm time-complexity segment-tree


【解决方案1】:
#include <iostream>
using namespace std;
typedef long long ll;
void updateSegementLazyTree(ll *tree , ll *lazy , ll low, ll high,ll startR ,ll endR ,ll updateValue ,ll treeNode)
{

    //before using current node we need to check weather currentnode has any painding updation or not
    if (lazy[treeNode]!=0)
    {
        //update painding updation
        tree[treeNode]  +=  (high-low+1)*lazy[treeNode];
        //transfer update record to child of current node if child possible
        if (low!=high)
        {
            //that's means child possible
            lazy[treeNode*2]    += lazy[treeNode];  //append update to left child
            lazy[treeNode*2+1]  += lazy[treeNode];  //append update to right child
        }
        lazy[treeNode]=0;//remove lazyness of current node
    }
    //if our current interval [low,high] is completely outside of the given Interval[startR,endR]
    if (startR >high || endR <low || low>high)
    {   
        //then we have to ignore those path of tree
        return;
    }
    //if our current interval is completely inside of given interval
    if (low >=startR && high <=endR)
    {
        //first need to update the current node with their painding updation
        tree[treeNode]  += (high-low+1)*updateValue;
        if (low!=high)
        {
            //that's means we are at the non-leaf node
            lazy[treeNode*2] +=updateValue; //so append lazyness to their left child
            lazy[treeNode*2+1] +=updateValue;//append lazyness to their right child
        }
        return;
    }
    //partially inside and outside then we have to traverse all sub tree i.e. right subtree and left subtree also
    ll mid=(low+high)/2;
    updateSegementLazyTree(tree ,  lazy ,  low,  mid, startR , endR , updateValue , treeNode*2);
    updateSegementLazyTree(tree ,  lazy ,  mid+1,  high, startR , endR , updateValue , treeNode*2+1);

    //while poping the function from stack ,we are going to save what i have done....Ok!!!!
    //update tree node:-
    tree[treeNode]  = tree[treeNode*2] + tree[treeNode*2+1];    //left sum+rightsum(after updation)
}
ll getAnswer(ll *tree ,ll * lazy , ll low, ll high ,ll startR,ll endR , ll treeNode)
{
    //base case
    if (low>high)
    {
        return 0;
    }
    //completely outside
    if (low >endR || high <startR)
    {
        return 0;
    }
    //before using current node we need to check weather currentnode has any painding updation or not
    if (lazy[treeNode]!=0)
        {
            //i.e. if we would have added x value from low to high then total changes for root node will be (high-low+1)*x
            tree[treeNode]  += (high-low+1)*lazy[treeNode];
            if (low!=high)
            {
                //if we are at non-leaf node
                lazy[treeNode*2] += lazy[treeNode]; //append updateion process to left tree
                lazy[treeNode*2+1] += lazy[treeNode];//append updation process to right tree
            }
            lazy[treeNode]=0;
        }
    //if our current interval is completely inside of given interval
    if (low >=startR && high <=endR)
    {
        return tree[treeNode];
    }
    //if our current interval is cpartially inside and partially out side of given interval then we need to travers both side left and right too
    ll mid=(low+high)/2;
    if(startR>mid)
    {
        //that's means our start is away from mid so we need to treverse in right subtree
        return getAnswer( tree ,  lazy ,  mid+1, high, startR, endR ,  treeNode*2+1);
    }else if(endR <= mid){
        //that's means our end is so far to mid or equal so need to travers in left subtree
        return getAnswer( tree ,  lazy ,  low, mid, startR, endR ,  treeNode*2);
    }
    ll left=getAnswer( tree ,  lazy ,  low, mid, startR, endR ,  treeNode*2);   //traverse right
    ll right=getAnswer( tree ,  lazy ,  mid+1, high, startR, endR ,  treeNode*2+1); //and left
    return (left+right);//for any node total sum=(leftTreeSum+rightTreeSum)
}
int main()
{
    int nTestCase;
    cin>>nTestCase;
    while(nTestCase--)
    {
        ll n,nQuery;
        cin>>n>>nQuery;
        ll *tree=new ll[3*n]();
        ll *lazy=new ll[3*n]();
        while(nQuery--)
        {
            int choice;
            cin>>choice;
            if (choice==0)
            {
                ll startR,endR,updateValue;
                cin>>startR>>endR>>updateValue;
                //0:- start index , n-1 end index ,1 treeIndex tree is our segment tree and lazy is our lazy segment tree
                updateSegementLazyTree(tree , lazy , 0, n-1, startR-1 , endR-1 , updateValue , 1);
            // for (int i = 0; i < 3*n; ++i)
            //  {
            //      cout<<i<<"\t"<<tree[i]<<"\t"<<lazy[i]<<endl;
            //  }
            }else{
                ll startR,endR;
                cin>>startR>>endR;
                ll answer=getAnswer(tree , lazy , 0, n-1 , startR-1 , endR-1 , 1);
                cout<<answer<<endl;
            }
        }
    }
}

updateSegementLazyTree() 取两个大小为4*n 的数组,因为长度为log(n) 的可能节点的总数将是2*2^log(n),最多为4*n。然后我们还需要interval[startR,endr]updateValue,我们通过recrusion 维护它们。 Tree[treeNode] 代表左右所有元素的总和。

示例输入如下所示:

1
8 6
0 2 4 26
0 4 8 80
0 4 5 20
1 8 8 
0 5 7 14
1 4 8

因此,对于第一个查询,我们必须将 2-4 更新为 +26。我们没有更新 2-4 之间的所有元素,而是将其存储在惰性树中,每当我们访问树中的任何节点时,我们首先检查该节点是否有任何待处理的更新。如果没有待处理的更新,则完成并将其转移给他们的孩子。

q1:- 0 2 4 26
tree[0,78,78,0,26,52,0,0,0,26] 

尝试制作树索引;左tree(2*i+1)right(2*i+1) 第一个索引至少为 78,即树顶,因此从 [0,n-1] 开始,当前最大值为 78。

tree[treeNode] += (high-low+1)*lazy[treeNode];

如果我们将x从低索引添加到高索引,那么在整个子数组i中添加了(high-low+1)*x ; -1,因为索引来自0

然后,在访问树中的任何节点之前,我们会延迟检查该节点是否有任何待处理的更新。 if (lazy[treeNode]!=0) 如果有,则更新并懒惰转移给他们的孩子。对左子树和右子树也继续这样做。

然后我们在range[startR,endR] 内到达getAnswer() 正如我之前提到的,我们首先检查每个受影响节点的待处理更新。如果为 true,则完成更新并根据间隔递归调用左右子树。

最后我们得到根节点的leftSubtreerightsubtree 的总和,将它们相加并返回。

时间复杂度

在更新中,getAnswer(),在最坏的情况下将不得不遍历整个树,即树的高度O(2*Log N)2*log n 因为这是我们必须在左右子树中旅行的最坏情况,例如对于间隔[0-n-1]

对于k 查询,总体时间复杂度为O(K*log n)

【讨论】:

    【解决方案2】:

    您可以使用分段树。 http://www.geeksforgeeks.org/segment-tree-set-1-sum-of-given-range/

    #include <cstdio>
    
    const int MAX_N = 1000003;
    
    int tree[(1 << 21) + 3], a[MAX_N], N, M;
    int x, y;
    
    int query(int n, int l, int r) {
        if(l > y || r < x) {
            return 0;
        }
        if(l >= x && r <= y) {
            return tree[n];
        }
        int q = query(2*n, l, (l + r)/2);
        int p = query(2*n + 1, (l + r)/2 + 1, r);
        return p + q;
    }
    
    void init(int n, int l, int r) {
        if(l == r) {
            tree[n] = a[l];
            return;
        }
        init(2*n, l, (l + r)/2);
        init(2*n + 1, (l + r)/2 + 1, r);
        tree[n] = tree[2*n] + tree[2*n + 1];
    }
    
    void update(int n, int l, int r) {
        if(l > y || r < x)
            return;
        if(l == r && x == r) {
            tree[n] = y;
            return;
        }
        else if(l == r) {
            return;
        }
        update(2*n, l, (l + r)/2);
        update(2*n + 1, (l + r)/2 + 1, r);
        tree[n] = tree[2*n] + tree[2*n + 1];
    }
    int main() {
        scanf("%d%d", &N, &M);
        for(int i = 0; i < N; i++) {
            scanf("%d", &a[i]);
        }
        init(1, 0, N - 1);
        for(int i = 0; i < M; i++) {
            int c;
            scanf("%d%d%d", &c, &x, &y);
            if(!c) {
                a[x] = y;
                update(1, 0, N - 1);
            } else {
                printf("%d\n", query(1, 0, N - 1));
            }
        }
        return 0;
    }
    

    基本思想是将元素数组扩展为二叉树。该树的每个节点都保存有关其子节点元素总和的信息。通过应用这个技巧,你可以很容易地知道某个节点覆盖了哪个范围:

    Root 持有[1,N] 范围内的信息。 根的左孩子持有关于范围[1, int(N/2)] 的信息。 根的右孩子持有关于范围[int(N/2)+1, N]的信息。

    一般来说,如果节点“A”持有关于范围[l, r] 的信息。然后留下孩子 持有关于范围[l, int((l+r)/2)] 的信息,右孩子持有信息 关于范围[int((l+r)/2)+1, r]

    还有一个用数组表示二叉树的好技巧。 假设您将树保存在数组“树”中(就像我在做我的代码一样)。然后根 树将位于tree[1]。根的左孩子将是tree[2] 和树的右孩子 根将是tree[3]

    一般来说,如果你在节点 n 上,那么它的左孩子是 2*n,右孩子是 2*n + 1

    这就是为什么我用 (1, 0, N - 1) 调用我的 queryupdate 函数。我从根节点1 开始。我用那个节点 i [0, N-1] 覆盖的范围。而且我总是试图找到第一个节点,它适合我需要计算总和的范围。

    这是一个开始。尝试在谷歌上搜索更多关于分段树的信息。当您开始探索时,您会发现有几种方法可以表示您的树。

    祝你好运。 :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-31
      • 1970-01-01
      • 2023-01-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多