1. 程式人生 > 實用技巧 >[學習筆記]數列分塊入門九題[LOJ6277-6285]

[學習筆記]數列分塊入門九題[LOJ6277-6285]

Thoughts

打完這九題,感覺脫了一層皮,各種或毒瘤或傻逼的錯誤,很難只交一次便通過。如果不看題解把這九題打完,不僅分塊有所進步,調程式碼細節的能力也會提升。

做完這九題,讓我感覺到分塊演算法本身思維難度不大,主要是程式碼的細節問題。而要想用分塊解決一個問題,最終要的是找到每個塊到底儲存什麼,這些儲存的值能否合併,怎樣合併


Solution

T1

  • 大意

    區間加法與單點查值

  • 思路

    • 分塊入門第一題,是對分塊演算法的大致模式的練習。散塊暴力,整塊合併。
  • 程式碼

  #include <bits/stdc++.h>
  
  const int N = 50000;
  
  int n;
  int opt;
  int l;
  int r;
  int c;
  int len;
  int start;
  int end;
  int v[300];
  int a[N + 30];
  int p[N + 30];
  
  inline void build()
  {
      len = sqrt(n);
      for (int i = 1; i <= n; ++i)
          p[i] = (i - 1) / len + 1;
  }
  
  inline void add(int L, int R, int s)
  {
      if (p[L] == p[R])
          for (int i = L; i <= R; ++i)
              a[i] += s;
      else
      {
          for (int i = L; p[i] == p[L]; ++i)
              a[i] += s;
          for (int i = R; p[i] == p[R]; --i)
              a[i] += s;
          for (int i = p[L] + 1; i <= p[R] - 1; ++i)
              v[i] += s;
      }
  }
  
  int main()
  {
      std::cin >> n;
      for (int i = 1; i <= n; ++i)
          std::cin >> a[i];
      build();
      for (int i = 1; i <= n; ++i)
      {
          std::cin >> opt>> l>> r>> c;
          if (opt == 0)
              add(l, r, c);
          else
              printf ("%d\n", v[p[r]] + a[r]);
      }
      return 0;
  }

T2

  • 大意

    區間加法,詢問區間小於某個值\(x\)的元素個數

  • 思路

    • 對於操作2,我們無法在原塊上快速查詢答案,因此我們要儲存原塊的有序版本,對於每個塊內,都是有序的,但塊與塊之間沒有關聯。

    • 保證塊有序之後,我們依然對於散塊進行暴力,整塊則進行二分查詢,每次操作2的複雜度就變為了\(O(\sqrt{n} \log n)\)

    • 程式中的\(rebuild\)函式是在操作1暴力加散塊之後,重構散塊的有序版本

  • 程式碼:

  #include <vector>
  #include <cmath>
  #include <cstdio>
  #include <algorithm>
  
  const int N = 50030;
  const int Block = 500;
  
  int n, blo, opt, l, r, c, ans;
  int a[N], p[N], v[Block];
  std::vector <int> b[Block];
  
  inline void rebuild(int num)
  {
      b[num].clear();
      for (int i = blo * (num - 1) + 1; i <= std::min(blo * num, n); ++i)
          b[num].push_back(a[i]);
      std::sort(b[num].begin(), b[num].end());
  }
  
  int main()
  {
      scanf ("%d", &n);
      blo = sqrt(n);
      for (int i = 1; i <= n; ++i)
      {
          scanf ("%d", &a[i]);
          p[i] = (i - 1) / blo + 1;
          b[p[i]].push_back(a[i]);
      }
      for (int i = 1; i <= p[n]; ++i)
          std::sort(b[i].begin(), b[i].end());
      for (int t = 1; t <= n; ++t)
      {
          scanf ("%d%d%d%d", &opt, &l, &r, &c);
          if (opt == 0)
          {
              for (int i = l; i <= std::min(blo * p[l], r); ++i)
                  a[i] += c;
              rebuild(p[l]);
              if (p[l] != p[r])
              {hebing
                  for (int i = r; i > (p[r] - 1) * blo; --i)
                      a[i] += c;
                  rebuild(p[r]);
              }
              for (int i = p[l] + 1; i < p[r]; ++i)
                  v[i] += c;
          }
          else
          {
              ans = 0;
              c *= c;
              for (int i = l; i <= std::min(r, blo * p[l]); ++i)
                  ans += (a[i] + v[p[i]] < c);
              for (int i = r; i > (p[r] - 1) * blo && p[l] != p[r]; --i)
                  ans += (a[i] + v[p[i]] < c);
              for (int i = p[l] + 1; i < p[r]; ++i)
                  ans += std::lower_bound(b[i].begin(), b[i].end(), c - v[i]) - b[i].begin();
              printf ("%d\n", ans);
          }
      }
      return 0;
  }

T3

  • 大意

    區間加法,詢問區間內比某個值\(x\)小的最大元素

  • 思路

    • 與第2題相似,只需把詢問小於\(x\)元素個數改為詢問\(x\)前驅
  • 程式碼

  #include <vector>
  #include <cmath>
  #include <cstdio>
  #include <algorithm>
  
  const int N =100030;
  const int Block = 500;
  
  int n, blo, opt, l, r, c, x, ans;
  int a[N], p[N], v[Block];
  std::vector <int> b[Block];
  
  inline void rebuild(int num)
  {
      b[num].clear();
      for (int i = blo * (num - 1) + 1; i <= std::min(blo * num, n); ++i)
          b[num].push_back(a[i]);
      std::sort(b[num].begin(), b[num].end());
  }
  
  int main()
  {
      scanf ("%d", &n);
      blo = sqrt(n);
      for (int i = 1; i <= n; ++i)
      {
          scanf ("%d", &a[i]);
          p[i] = (i - 1) / blo + 1;
          b[p[i]].push_back(a[i]);
      }
      for (int i = 1; i <= p[n]; ++i)
          std::sort(b[i].begin(), b[i].end());
      for (int t = 1; t <= n; ++t)
      {
          scanf ("%d%d%d%d", &opt, &l, &r, &c);
          if (opt == 0)
          {
              for (int i = l; i <= std::min(blo * p[l], r); ++i)
                  a[i] += c;
              rebuild(p[l]);
              if (p[l] != p[r])
              {
                  for (int i = r; i > (p[r] - 1) * blo; --i)
                      a[i] += c;
                  rebuild(p[r]);
              }
              for (int i = p[l] + 1; i < p[r]; ++i)
                  v[i] += c;
          }
          else
          {
              ans = -1;
              for (int i = l; i <= std::min(r, blo * p[l]); ++i)
                  if (a[i] + v[p[i]] < c)
                      ans = std::max(a[i] + v[p[i]], ans);
              for (int i = r; i > (p[r] - 1) * blo && p[l] != p[r]; --i)
                  if (a[i] + v[p[i]] < c)
                      ans = std::max(a[i] + v[p[i]], ans);
              for (int i = p[l] + 1; i < p[r]; ++i)
                  if (c - v[i] > b[i][0])
                  {
                      x = std::lower_bound(b[i].begin(), b[i].end(), c - v[i]) - b[i].begin();
                      ans = std::max(b[i][x - 1] + v[i], ans);
                  }
              printf ("%d\n", ans);
          }
      }
      return 0;
  }

T4

  • 大意

    區間加法與區間求和

  • 思路

    • 儲存每個塊內的元素和,散塊暴力,整塊合併
  • 程式碼

  #include <vector>
  #include <cmath>
  #include <cstdio>
  #include <algorithm>
  
  const int N =100030;
  const int Block = 500;
  typedef long long LL;
  
  int n, blo, opt, l, r, x;
  int  p[N];
  LL ans, c;
  LL a[N], v[Block], b[Block];
  
  int main()
  {
      scanf ("%d", &n);
      blo = sqrt(n);
      for (int i = 1; i <= n; ++i)
      {
          scanf ("%lld", &a[i]);
          p[i] = (i - 1) / blo + 1;
          b[p[i]] += a[i];
      }
      for (int t = 1; t <= n; ++t)
      {
          scanf ("%d%d%d%lld", &opt, &l, &r, &c);
          if (opt == 0)
          {
              for (int i = l; i <= std::min(blo * p[l], r); ++i)
              {
                  a[i] += c;
                  b[p[i]] += c;
              }
              for (int i = r; i > (p[r] - 1) * blo && p[l] != p[r]; --i)
              {
                  a[i] += c;
                  b[p[i]] += c;
              }
              for (int i = p[l] + 1; i < p[r]; ++i)
                  v[i] += c;
          }
          else
          {
              ans = 0;
              for (int i = l; i <= std::min(r, blo * p[l]); ++i)
                  ans = (ans + a[i] + v[p[i]]) % (c + 1);
              for (int i = r; i > (p[r] - 1) * blo && p[l] != p[r]; --i)
                  ans = (ans + a[i] + v[p[i]]) % (c + 1);
              for (int i = p[l] + 1; i < p[r]; ++i)
                  ans = (ans + b[i] + v[i] * blo) % (c + 1);
              printf ("%lld\n", ans);
          }
      }
      return 0;
  }

T5

  • 大意

    區間開方,區間求和

    洛谷相似題目

  • 思路

    • 每個數開方,開到幾次就變成1了,顯然可以暴力。

    • 只需記錄下每個塊內是否都開完了(即是否全變1了),開完了的話就跳過,如果沒有就暴力開。

  • 程式碼

  #include <vector>
  #include <cmath>
  #include <cstdio>
  #include <algorithm>
  
  const int N =100030;
  const int Block = 500;
  typedef long long LL;
  
  int n, blo, opt, l, r, c, x;
  int p[N], a[N], cnt[Block];
  LL ans;
  
  int main()
  {
      scanf ("%d", &n);
      blo = sqrt(n);
      for (int i = 1; i <= n; ++i)
      {
          scanf ("%d", &a[i]);
          p[i] = (i - 1) / blo + 1;
          cnt[p[i]] += (a[i] == 1);
      }
      for (int t = 1; t <= n; ++t)
      {
          scanf ("%d%d%d%d", &opt, &l, &r, &c);
          if (opt == 0)
          {
              for (int i =hebing l; i <= std::min(blo * p[l], r); ++i)
              {
                  if (a[i] > 1)
                  {
                      a[i] = sqrt(a[i]);
                      cnt[p[i]] += (a[i] == 1);
                  }
              }
              for (int i = r; i > (p[r] - 1) * blo && p[l] != p[r]; --i)
              {
                  if (a[i] > 1)
                  {
                      a[i] = sqrt(a[i]);
                      cnt[p[i]] += (a[i] == 1);
                  }
              }
              for (int i = p[l] + 1; i < p[r]; ++i)
                  for (int j = (i - 1) * blo + 1; j <= std::min(n, i * blo) && cnt[i] != blo; ++j)
                      if (a[j] > 1)
                      {
                          a[j] = sqrt(a[j]);
                          cnt[p[j]] += (a[j] == 1);
                      }
          }
          else
          {
              ans = 0;
              for (int i = l; i <= std::min(r, blo * p[l]); ++i)
                  ans += a[i];
              for (int i = r; i > (p[r] - 1) * blo && p[l] != p[r]; --i)
                  ans += a[i];
              for (int i = p[l] + 1; i < p[r]; ++i)
                  if (cnt[i] == blo)
                      ans += blo;
                  else
                      for (int j = (i - 1) * blo + 1; j <= std::min(n, i * blo); ++j)
                          ans += a[j];
              printf ("%lld\n", ans);
          }
      }
      return 0;
  }

T6

  • 大意

    單點插入,單點查詢

  • 思路

    • 本題每個塊用一個\(vector\),要插入某元素,則在其所在塊用\(insert\)函式插入
    • 要想找到其所在塊,只需從第一個塊開始遍歷,每次給插入位置減去當前塊的大小即可
    • 值得一提的是,這樣做無法保證塊的大小都相同,如果本題在某一個塊中插入的元素個數過多,比如說我每次都插到同一個塊裡去,那個塊大小就變得很大,這樣就會超時。解決辦法則是進行定期重構,當插入了\(\sqrt{n}\)元素時,就對所有塊進行重構,調整每個塊的大小

    不過這個題只開一個塊,直接用vector進行insert也能過。。。。

  • 程式碼

    這題我壓行了,,,,,,,

  #include <bits/stdc++.h>
  const int N =100030;
  int n, blo, opt, l, r, c, x, m, num, sum, top, a[N], st[N * 2];
  std::vector < int > b[N];
  inline void rebuild() {
      sum = top = 0;
      for (int i = 1; i <= m; ++i) {
          for (std::vector < int >::iterator it = b[i].begin(); it != b[i].end(); ++it) st[++top] = *it;
          b[i].clear();
      }int block2 = sqrt(top);
      for (int i = 1; i <= top; ++i) b[(i - 1) / block2 + 1].push_back(st[i]);
      m = (top - 1) / block2 + 1;
  }int main() {
      scanf ("%d", &n);
      blo = sqrt(n);
      for (int i = 1; i <= n; ++i){
          scanf ("%d", &a[i]);
          b[(i - 1) / blo + 1].push_back(a[i]);
      }m = (n - 1) / blo + 1;
      for (int t = 1; t <= n; ++t) {
          scanf ("%d%d%d%d", &opt, &l, &r, &c);
          if (opt == 0){
              num = 1;
              while (l > b[num].size()) l -= b[num++].size();
              b[num].insert(b[num].begin() + l - 1, r);
              if (++sum == blo) rebuild();
          }else {
              x = 1;
              while (r > b[x].size()) r -= b[x++].size();
              printf ("%d\n", b[x][r - 1]);
          }
      }return 0;
  }

T7

  • 大意

    區間乘法,區間加法,單點詢問

  • 思路

    • 兩個標記,一個加法,一個乘法,先乘後加,在進行暴力散塊時,要把散塊所在的整個塊的標記下傳後再進行暴力乘法加法
  • 程式碼

    這題我又壓了,壓行真的還挺爽的

#include <bits/stdc++.h>
const int N = 1000000, Blo = 5000, MOD = 10007;
int n, l, r, c, opt, blo, p[N], fir[Blo], end[Blo];
long long add[Blo], mul[Blo], a[N];
int main() {
    scanf ("%d", &n); blo = sqrt(n);
    for (int i = 1; i <= n; ++i) {
        scanf ("%lld", &a[i]);
        p[i] = (i - 1) / blo + 1;
        if (fir[p[i]] == 0) fir[p[i]] = i;
        end[p[i]] = i; mul[p[i]] = 1; 
    } for (int i = 1; i <= n; ++i) {
        scanf ("%d%d%d%d", &opt, &l, &r, &c);
        if (opt <= 1) {
            for (int i = fir[p[l]]; i <= end[p[l]]; ++i) {
                if (i < l || i > std::min(r,end[p[l]])) a[i] = (a[i] * mul[p[i]] + add[p[i]]) % MOD;
                else if (opt == 0) a[i] = (a[i] * mul[p[i]] + add[p[i]] + c) % MOD;
                else a[i] = (a[i] * mul[p[i]] * c + add[p[i]] * c) % MOD;
                if (i == end[p[l]]) {mul[p[i]] = 1; add[p[i]] = 0;}
            }for (int i = end[p[r]]; i >= fir[p[r]] && p[l] != p[r]; --i) {
                if (i > r) a[i] = (a[i] * mul[p[i]] + add[p[i]]) % MOD;
                else if (opt == 0) a[i] = (a[i] * mul[p[i]] + add[p[i]] + c) % MOD;
                else a[i] = (a[i] * mul[p[i]] * c + add[p[i]] * c) % MOD; 
                if (i == fir[p[r]]) {mul[p[i]] = 1; add[p[i]] = 0;}
            }for (int i = p[l] + 1; i < p[r]; ++i)
                if (opt == 0) add[i] = (add[i] + c) % MOD;
                else {mul[i] = (mul[i] * c) % MOD; add[i] = (add[i] * c) % MOD; }
        }else printf ("%lld\n", (a[r] * mul[p[r]] + add[p[r]]) % MOD);
    }return 0;
}

T8

  • 大意

    區間詢問等於一個數\(c\)的元素個數,再把這個區間內所有元素改為\(c\)

  • 思路

    • 記錄下這個區間是否的元素是否全是同一個數,是則賦值為該值,不是則賦值為INF
  • 程式碼

  #include <cstdio>
  #include <algorithm>
  #include <cmath>
  
  typedef long long LL;
  
  const int N =  1e5 + 30, Blo = 5000;
  const LL INF = 1e12 + 30;
  
  int n, l, r, c, blo, ans;
  int p[N];
  LL a[N], now[Blo];
  
  int main()
  {
      scanf ("%d", &n);
      blo = sqrt(n);
      for (int i = 1; i <= n; ++i)
      {
          scanf ("%lld", &a[i]);
          p[i] = (i - 1) / blo + 1;
          now[p[i]] = INF;
      }
      
      for (int t = 1; t <= n; ++t)
      {
          ans = 0;
          scanf ("%d%d%d", &l, &r, &c);
          for (int i = (p[l] - 1) * blo + 1; i <= std::min(n, p[l] * blo); ++i)
          {
              if ((i < l || i > r) && now[p[i]] != INF) 
                  a[i] = now[p[i]];
              else if (i >= l && i <= r)
              {
                  ans += ((a[i] == c && now[p[i]] == INF) || now[p[i]] == c);
                  a[i] = c;
              }
          }
          
          for (int i = (p[r] - 1) * blo + 1; i <= std::min(n, p[r] * blo) && p[l] != p[r]; ++i)
          {
              if (i > r && now[p[i]] != INF)
                  a[i] = now[p[i]];
              else if (i <= r && i >= l)
              {
                  ans += ((a[i] == c && now[p[i]] == INF) || now[p[i]] == c);
                  a[i] = c;
              }
          }
          now[p[l]] =  now[p[r]] = INF;
  
          for (int i = p[l] + 1; i < p[r]; ++i)
          {
              if (now[i] == c) ans += blo;
              else if (now[i] == INF)
              {
                  for (int j = (i - 1) * blo + 1; j <= i * blo; ++j)
                      ans += (a[j] == c);
              }
              now[i] = c;
          }
          printf ("%d\n", ans);
      }
  
      return 0;
  }

T9

  • 大意

    區間最小眾數

  • 思路

    • 考慮區間最小眾數,答案肯定為區間內整塊部分的唯一一個最小眾數或者是散塊裡所有出現過的數中的一個

    • 為什麼呢,理性證明請百度陳立傑的區間最小眾數解題報告。這裡我們感性理解下,對於整塊部分的那個眾數,不用想,它肯定是有可能成為答案的,這個時候任何一個整塊部分裡的數都無法替代它,替代它的唯一可能就是再增加自己的出現次數,這種情況是隻有它在散塊出現裡才有可能的。

  • 程式碼

  #include <cstdio>
  #include <cmath>
  #include <algorithm>
  #include <map>
  #include <cstring>
  #include <vector>
  
  const int N = 100030;
  const int BLOCK = 2030;
  
  bool vis[N];
  int n, blo, id, mx, f, l, r, x;
  int a[N], Ans[N], m[BLOCK][BLOCK], p[N], num[N];
  std::map <int, int > mp;
  std::vector < int > v[N];
  
  int get(int left, int right, int now)
  {
      return std::upper_bound(v[now].begin(), v[now].end(), right) - std::lower_bound(v[now].begin(), v[now].end(), left); 
  }
  
  int main()
  {
      scanf ("%d", &n);
      blo = 80;
      for (int i = 1; i <= n; ++i)
      {
          scanf ("%d", &a[i]);
          if (mp[a[i]] == 0)
          {
              mp[a[i]] = ++id;
              Ans[id] = a[i];
          }
          p[i] = (i - 1) / blo + 1;
          a[i] = mp[a[i]];
          v[a[i]].push_back(i);
      }
      for (int i = 1; i <= p[n]; ++i)
      {
          std::memset(num, 0 ,sizeof(num));
          mx = f = 0; 
          for (int j = (i - 1) * blo + 1; j <= n; ++j)
          {
              num[a[j]]++;
              if (num[a[j]] > f || (num[a[j]] == f && Ans[a[j]] < Ans[mx]))
              {
                  mx = a[j];Markdown Theme Kit
      }
      for (int t = 1; t <= n; ++t)
      {
          scanf ("%d%d", &l, &r);
          mx = get(l, r, m[p[l] + 1][p[r] - 1]);
          f = m[p[l] + 1][p[r] - 1];
          std::memset(vis, 0, sizeof(vis));
          vis[f] = 1;
          for (int i = l; i <= std::min(r, p[l] * blo); ++i)
          {
              if (vis[a[i]] == 0)
              {
                  vis[a[i]] = 1;
                  x = get(l, r, a[i]);
                  if (mx < x || mx == x && Ans[a[i]] < Ans[f])
                  {
                      f = a[i];
                      mx = x;
                  }
              }
          }
          for (int i = (p[r] - 1) * blo + 1; i <= r && p[l] != p[r]; ++i)
          {
              if (vis[a[i]] == 0)
              {
                  vis[a[i]] = 1;
                  x = get(l, r, a[i]);
                  if (mx < x || mx == x && Ans[a[i]] < Ans[f])
                  {
                      f = a[i];
                      mx = x;
                  }
              }
          }
          printf ("%d\n", Ans[f]);
      }
      return 0;
  }

Ending

碼字不易,歡迎點贊和評論qvq