1. 程式人生 > 實用技巧 >Codeforces Round #579 (Div. 3) D2. Remove the Substring (hard version) (思維,貪心)

Codeforces Round #579 (Div. 3) D2. Remove the Substring (hard version) (思維,貪心)

樹狀陣列

一、適用範圍

  • 樹狀陣列是一個查詢和修改複雜度都為 \(log(n)\) 的資料結構,常常用於查詢任意區間的所有元素之和。
  • 與字首和的區別是支援動態修改, \(log(n)\) 的時間進行修改,\(log(n)\) 查詢。
  • 支援如下操作:
    • 單點修改區間查詢
    • 區間修改單點查詢
    • 區間修改區間查詢

二、演算法原理

  1. 樹狀陣列較好的利用了二進位制。它的每個節點的值代表的是自己前面一些連續元素。至於到底是前面哪些元素,這就由這個節點的下標決定。

  1. 設節點的編號為 \(i\) ,那麼:

\[c[i]=\sum_{j=i-lowbit(i)+1}^i a[j] \]

  1. 即可以推匯出:

    C[1] = A[1]  # lowbit(1)個元素之和
    C[2] = C[1] + A[2] = A[1] + A[2]  # lowbit(2)個元素之和
    C[3] = A[3]  # lowbit(3)個元素之和
    C[4] = C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4] # lowbit(4)個元素之和
    C[5] = A[5]
    C[6] = C[5] + A[6] = A[5] + A[6]
    C[7] = A[7]
    C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
    
  2. 顯然一個節點並不一定是代表自己前面所有元素的和。只有滿足 \(2^n\) 這樣的數才代表自己前面所有元素的和。

  3. 理解 \(lowbit\) 函式

    • 原碼:如果機器字長為 \(n\),那麼一個數的原碼就是用一個 \(n\) 位的二進位制數,其中最高位為符號位:正數為 \(0\),負數為 \(1\)。剩下的 \(n-1\) 位表示該數的絕對值。

    • 反碼:知道了原碼,那麼你只需要具備區分 \(0\)\(1\) 的能力就可以輕鬆求出反碼,為什麼呢?因為反碼就是在原碼的基礎上,符號位不變其他位按位取反(就是 \(0\)\(1\)\(1\)\(0\))就可以了。

    • 補碼也非常的簡單,就是在反碼的基礎上按照正常的加法運算加 \(1\)

      。正數的補碼就是其本身。負數的補碼是在其原碼的基礎上符號位不變,其餘各位取反,最後 \(+1\),即取反 \(+1\)

    • $lowbit(x)=x&-x $ :表示擷取 \(x\) 二進位制最右邊的 \(1\) 所表示的值,可以寫成函式或巨集定義

    • 注意巨集定義是括號,因為巨集名只是起到一個替代作用,不加括號在運算時優先順序會出問題

      //1. 巨集定義,注意括號,不建議這樣寫,容易產生歧義
      #define lowbit(x) ((x) & -(x))
      //2. 函式寫法,推薦寫法:
      int lowbit(int x){return x & -x;}
      

三、 樹狀陣列的操作

  1. \(update\) 更新操作

    • 因為樹狀陣列 \(c[x]\) 維護的是一個或若干個連續數之和,當我們修改了 \(a[x]\) 之後,\(x\sim n\) 字首和均發生了變化,所以除了\(c[x]\) 需要修改之外 \(x\) 的祖先節點也必須修改而 \(x\) 的父親節點為 \(x+lowbit(x)\),我們叫向上更新。

    • 把序列中第 \(i\) 個數增加 \(x\)\(sum[i]\sim sum[n]\) 均增加了 \(x\) ,所以我們只需把這個增量往上更新即可。如果,把 \(a[i]\) 修改成 \(x\),則我們向上更新 \(a[i]\) 的增量:\(x-a[i]\)

      //1. a[id] 增加 x while寫法
      void updata(int id,int x){
          while(id<=n){//向上更新,更新到n為止
              c[id]+=x;
              id+=lowbit(id);
          }
      }
      //2. a[id] 修改成 x  for寫法
      void updata(int id,int x){//或者傳遞引數是x=x-a[id],此時跟第一種寫法一樣
          for(int i=id;i<=n;i+=lowbit(i))
              c[i]+=x-a[id];
      }
      
  2. \(getsum\) 查詢操作

    • 因為樹狀陣列維護的是一個能夠動態修改的字首和,所以可以在 \(log(n)\) 的效率下求出前 \(n\) 項和\(sum[i]\)

    • 如果 \(i=2^j (j=0,1,..n)\), 此時最簡單,顯然有:\(sum[i]=c[i]\) ,如果 \(i\) 是其他的情況呢?

      • \(sum[5]=c[5]+c[4]\ (4=5-lowbit(5))\)
      • \(sum[15]=c[15]+c[14]+c[12]+c[8]\ (14=15-lowbit(15),12=14-lowbit(14),...)\)
    • 顯然,想要求出前 \(i\) 項字首和 \(sum[i]\) ,只需沿著當前節點向下累加直到節點編號為 \(2^j\) 為止。我們叫向下求和。

      int getsum(int id){
          int tot=0;
          for(int i=id;i>0;i-=lowbit(i))
              tot+=c[i];
          return tot;
      }