逐月資訊學——2024初秋集訓——提高組 #22

Yaosicheng124發表於2024-09-08

A. 牛牛的方程式

題目描述

給定一個三元一次方程 \(ax+by+cz=d\),求該方程是否存在整數解。

思路

由於若干個 \(a,b,c\) 只能湊出 \(\gcd (a,b,c)\) 的倍數,所以只需判斷 \(d\) 是否為 \(\gcd(a,b,c)\) 的倍數即可。特別的,若 \(a,b,c\) 均為 \(0\),則顯然只有 \(d=0\) 時存在整數解。

時空複雜度均為 \(O(1)\)

程式碼

#include<bits/stdc++.h>
using namespace std;
using ll = long long;

int t;
ll a, b, c, d;

ll gcd(ll a, ll b) {
  return (!b ? a : gcd(b, a % b));
}

void Solve() {
  cin >> a >> b >> c >> d;
  cout << (!a && !b && !c ? (!d ? "YES\n" : "NO\n") : (d % gcd(gcd(a, b), c) == 0 ? "YES\n" : "NO\n"));
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  for(cin >> t; t--; Solve()) {
  }
  return 0;
}

B. 牛牛的猜球遊戲

題目描述

有一排 \(10\) 個球,依次編號 \(0-9\),有 \(N\) 種操作,每次操作交換球 \(a,b\)。給定 \(M\) 次查詢,每次求依次進行完 \(l\)\(r\) 的操作後每個位置上球的編號。

思路

由於此題沒有修改,所以考慮倍增。

\(f_{i,j}\) 表示從 \(i\) 開始,進行 \(2^j\) 次操作後每個球所處的位置。

我們很明顯有以下轉移:\(f_{i,j,_k}=f_{i+2^{j-1},j-1,f_{i,j-1,k}}\)

查詢時直接倍增求解即可。

空間複雜度 \(O(N\log N)\),時間複雜度 \(O(N \log N + M\log N)\)

程式碼

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 100001;

int n, m, f[18][MAXN][11], res[11], ans[11];

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for(int i = 1, a, b; i <= n; ++i) {
    cin >> a >> b;
    a++, b++;
    for(int j = 1; j <= 10; ++j) {
      f[0][i][j] = j;
    }
    swap(f[0][i][a], f[0][i][b]);
  }
  for(int i = 1; i <= 17; ++i) {
    for(int j = 1; j <= n; ++j) {
      if(j + (1 << i) - 1 <= n) {
        for(int k = 1; k <= 10; ++k) {
          f[i][j][k] = f[i - 1][j + (1 << (i - 1))][f[i - 1][j][k]];
        }
      }
    }
  }
  for(int i = 1, l, r; i <= m; ++i) {
    cin >> l >> r;
    for(int j = 1; j <= 10; ++j) {
      res[j] = j;
    }
    int x = l;
    for(int j = 17; j >= 0; --j) {
      if(x + (1 << j) - 1 <= r) {
        for(int k = 1; k <= 10; ++k) {
          res[k] = f[j][x][res[k]];
        }
        x += (1 << j);
      }
    }
    for(int j = 1; j <= 10; ++j) {
      ans[res[j]] = j;
    }
    for(int j = 1; j <= 10; ++j) {
      cout << ans[j] - 1 << " \n"[j == 10];
    }
  }
  return 0;
}

C. 牛牛的湊數遊戲

題目描述

對於一個多重集合 \(S\),若 \(\exists S' \subseteq S\)\(\sum \limits_{x\in S'} x = v\),則我們說 \(S\) 可以表示 \(v\)

給定一個長度為 \(N\) 的序列 \(A\)\(M\) 次查詢,每次查詢若 \(S=\{A_l,A_{l+1},\dots,A_r\}\)\(S\) 最小不能表示的非負整數。

思路

我們先考慮最暴力的一個 dp:

\(dp_i\) 表示考慮到第 \(i\) 個數,每個數是/否能表示出。

很明顯有 \(dp_{i}=dp_{i-1} \operatorname{or} (dp_{i-1} \operatorname{lsh} A_i)\),這裡 \(\operatorname{or},\operatorname{lsh}\) 分別表示位或,左移運算。

由於轉移的順序不會改變結果,所以考慮將 \(A\) 排序。

假設此時 \(dp_i\) 二進位制下是 \(X\dots X01\dots 1\),這裡令最低位的 \(0\) 在第 \(k\) 位。則在 \(0\)\(k-1\) 位都是 \(1\),所以此時最小不能表示的非負整數為 \(k\)

現在考慮轉移到 \(dp_{i+1}\),此時只要 \(A_{i+1}>k\),則永遠也湊不出 \(k\) 了,因為此時左移會跳過 \(k\),又因為 \(A\) 已被排序,所以之後也不會湊出來。此時答案就是 \(k\)

只要此時沒有 \(A_i > k\),我們就能一直加下去。為了加快速度,每次我們讓 \(k\leftarrow 小於等於 k 的數字之和\),因為 \(k\) 可以加上所有 \(\le k 且 > lastk\) 的數,這裡 \(lastk\) 是上一次的 \(k\)

而如果 \(k=lastk\),則代表加不下去了,也就是答案為 \(k\)

但時間複雜度對不對呢?在這裡每次至少加上 \(lastk+1\),也就是 \(lastk\) 每次至少 \(\times 2\),那麼 \(k\) 也是如此,所以單次時間複雜度 \(O(\log^2 \max\{A_i\})\)

每次求 \(\le k\) 的數字之和用可持久化 \(01\) 字典樹即可。

空間複雜度 \(O(N\log \max \{A_i\})\),時間複雜度 \(O(N\log \max\{A_i\}+M\log^2 \max\{A_i\})\)

程式碼

#include<bits/stdc++.h>
using namespace std;
using ll = long long;

const int MAXN = 100001;

struct Persistent_01Trie {
  int tot = 0, ROOT[MAXN], son[2][62 * MAXN];
  ll cnt[62 * MAXN];
  void Insert(int x, int y, ll val) {
    ROOT[x] = ++tot;
    int u = ROOT[x], v = ROOT[y];
    for(int i = 60; i >= 0; --i) {
      son[(val >> i) & 1][u] = ++tot;
      son[!((val >> i) & 1)][u] = son[!((val >> i) & 1)][v];
      cnt[u] = cnt[v] + val;
      u = son[(val >> i) & 1][u], v = son[(val >> i) & 1][v];
    }
    cnt[u] = cnt[v] + val;
  }
  ll Getsum(int l, int r, ll val) {
    int u = ROOT[l - 1], v = ROOT[r];
    ll ans = 0;
    for(int i = 60; i >= 0; --i) {
      if((val >> i) & 1) {
        ans += cnt[son[0][v]] - cnt[son[0][u]], u = son[1][u], v = son[1][v];
      }else {
        u = son[0][u], v = son[0][v];
      }
    }
    return ans + cnt[v] - cnt[u];
  }
}tr;

int n, m;

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for(int i = 1, x; i <= n; ++i) {
    cin >> x;
    tr.Insert(i, i - 1, x);
  }
  for(int i = 1, l, r; i <= m; ++i) {
    cin >> l >> r;
    ll x = 0;
    for(; ; ) {
      ll last = x;
      x = tr.Getsum(l, r, x + 1);
      if(x == last) {
        break;
      }
    } 
    cout << x + 1 << "\n";
  }
  return 0;
}

D. 牛牛的 RPG 遊戲

題目描述

給定一個 \(N\times M\) 的網格圖,每個格子 \((i,j)\) 都有一個事件,每次事件如下:

  • 獲得 \(val(i,j)\) 的分數
  • 接下來每走一步都會獲得 \(buff(i,j)\) 的分數,直到觸發下個事件前都有這個效果。

每遇到一個格子你都能選擇是否觸發事件,每次你只能往右/下走,求從 \((1,1)\) 開始到 \((N,M)\) 能獲得的最大分數。

思路

首先考慮最暴力的 dp:

\(dp_{i,j}\) 表示從 \((1,1)\) 到達並觸發 \((i,j)\) 的最大分數。

我們有 \(dp_{i,j}\leftarrow dp_{x,y}+(i-x+j-y)\cdot buff(x,y)+val(i,j)\)

化簡一下:\(dp_{i,j}\leftarrow dp_{x,y}-(x+y)\cdot buff(x,y)+(i+j)\cdot buff(x,y)+val(i,j)\)

這裡最難處理的地方就是 \((i+j)\cdot buff(x,y)\),因為它既包含了 \(i,j\) 又包含了 \(x,y\)

\(val(i,j)\) 可以在最後處理,主要考慮前面的部分。

前面的部分可以看作在平面直角座標系中一條 \(y=kx+b\) 的直線,這裡 \(k=buff(x,y),b=dp_{x,y}-(x+y)\cdot buff(x,y)\),查詢時就相當於查詢在 \(x=i+j\) 處的最大 \(y\),這個可以用李超線段樹求解。

這裡考慮分治 dp:

image

這樣不斷分治下去,直到只有一列,這樣直接 dp,每列只會被訪問 \(\log N\) 次,每次時間複雜度 \(O(\log (N+M))\)

空間複雜度 \(O(NM)\),時間複雜度 \(O(NM \log^2 (N+M))\)

程式碼

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 200001, INF = (int)(2e9);

struct line {
  int k, b;
  int Get(int x) const {
    return x * k + b;
  }
};

struct Li_Segment_Tree {
  int tot;
  struct Node {
    int l, r, ls, rs;
    line Max;
  }t[MAXN * 16];
  void Clear(int l, int r) {
    t[tot = 1] = {l, r, 0, 0, line{0, -INF}};
  }
  void Insert(line l) {
    int u = 1;
    for(; t[u].l < t[u].r; ) {
      int mid = t[u].l + (t[u].r - t[u].l) / 2;
      if(l.Get(mid) > t[u].Max.Get(mid)) {
        swap(l, t[u].Max);
      }
      if(t[u].l + 1 == t[u].r || l.k == t[u].Max.k || l.b == -INF) {
        break;
      }
      if(l.k < t[u].Max.k) {
        if(!t[u].ls) {
          t[u].ls = ++tot, t[tot] = {t[u].l, mid, 0, 0, line{0, -INF}};
        }
        u = t[u].ls;
      }else {
        if(!t[u].rs) {
          t[u].rs = ++tot, t[tot] = {mid, t[u].r, 0, 0, line{0, -INF}};
        }
        u = t[u].rs;
      }
    }
  }
  int Getmax(int x) {
    int u = 1, l = 1, r = MAXN, ret = 0;
    for(; u && t[u].l < t[u].r; ) {
      int mid = l + (r - l) / 2;
      if(t[u].Max.Get(x) > ret) {
        ret = t[u].Max.Get(x);
      }
      if(t[u].l + 1 == t[u].r) {
        break;
      }
      (x < mid ? (u = t[u].ls, r = mid) : (u = t[u].rs, l = mid));
    }
    return ret;
  }
}tr;

int n, m;
vector<int> b[MAXN], v[MAXN], dp[MAXN];

void dfs(int l, int r) {
  if(l == r) {
    tr.Clear(1, MAXN);
    for(int i = 1; i <= n; ++i) {
      dp[i][l] = max(dp[i][l], tr.Getmax(i + l) + v[i][l]);
      tr.Insert(line{b[i][l], dp[i][l] - (i + l) * b[i][l]});
    }
    return;
  }
  int mid = (l + r) >> 1;
  dfs(l, mid);
  tr.Clear(1, MAXN);
  for(int i = 1; i <= n; ++i) {
    for(int j = l; j <= mid; ++j) {
      tr.Insert(line{b[i][j], dp[i][j] - (i + j) * b[i][j]});
    }
    for(int j = mid + 1; j <= r; ++j) {
      dp[i][j] = max(dp[i][j], tr.Getmax(i + j) + v[i][j]);
    }
  }
  dfs(mid + 1, r);
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for(int i = 1; i <= n; ++i) {
    b[i].resize(m + 1), v[i].resize(m + 1), dp[i].resize(m + 1);
    for(int j = 1; j <= m; ++j) {
      cin >> b[i][j];
      dp[i][j] = -INF;
    }
  }
  dp[1][1] = 0;
  for(int i = 1; i <= n; ++i) {
    for(int j = 1; j <= m; ++j) {
      cin >> v[i][j];
    }
  }
  dfs(1, m);
  cout << dp[n][m];
  return 0;
}

相關文章