JOI Open 2016

hztmax0發表於2024-09-13

T1 JOIRIS

你在玩俄羅斯方塊,遊戲區域是一個寬度為 \(n\),高度足夠大的矩形網格、初始時第 \(i\) 列有 \(a_i\) 個方塊。
給定引數 \(k\),你可以做不超過 \(10^4\) 次操作,來將這個網格中的所有方塊全部消除,一次操作形如:

  • 在網格的最頂端落下一個 \(1 \times k\) 或者 \(k \times 1\) 的方塊(也就是你可以決定方塊是豎著放還是橫著放),直到碰到一個方塊時停止下落。
  • 自下而上檢查所有行,如果一行被方塊填滿,則消除這一行的所有方塊。

需要構造方案。保證 \(1 \le n, a_i \le 50\)

首先注意到我們可以透過若干操作使得 \(a_i \gets a_i \bmod k\)。令 \(t = \lfloor \frac{\max a_i}{k} \rfloor\),透過對於每個位置 \(i\),在 \(i\) 處加入若干豎塊,使得 \(a_i \in [tk, (t + 1)k)\),此時至少會消除 \(tk\) 次,於是 \(\forall i, a_i < k\)。我們稱這樣的操作為一次調整。

考慮 \(a\)\(k\) 意義下的差分陣列 \(d_{1 \sim n + 1}\),其滿足 \(d_i = a_i - a_{i - 1}\),對於一種合法的終態,由於所有方塊將被消空,所以 \(\forall i \in [2, n], d_i = 0\)。注意我們在 \(d_1\)\(d_{n + 1}\) 處無限制。

現在從 \(d\) 的角度觀察所有操作,現在加豎塊 \(d\) 不變,消除一行 \(d_{2 \sim n}\) 不變,沒有影響,唯一對 \(d\) 有影響的操作是在 \(i\) 處加一個橫塊,其影響為 \(d_i \gets d_i + 1, d_{i + k} \gets d_{i + k} - 1\),那麼有解的必要條件是 \(\forall r \in [0, k), 1 \bmod k \neq r, (n + 1) \bmod k \neq r, (\sum\limits_{i \bmod k = r} d_i) \equiv 0 \pmod k\),下面我們透過構造證明其是充分的。

考慮我們一定可以透過如下操作使得 \(d_i \gets d_i + v\)\(d_{i + k} \gets d_{i + k} - v\)\(v \in [0, k)\)):

  • \(i\) 處加入 \(v\) 個橫塊,在 \(\forall i \in [1, n] - [i, i + k)\) 處加入 \(2\) 個豎塊,此時 \(v\) 個橫塊全部被消除,如果剩下部分存在高度大於 \(k\),即 \(a_i \ge k\) 的位置,按照前面所說的方法調整即可。

那麼透過上述操作,對於 \(\bmod k = r\) 分組後 \(\sum d\) 不變,但是對於 \(1\)\(n + 1\) 所在的 \(r\) 我們可以全部丟到 \(1\)\(n + 1\),所以沒有前面的限制。

操作次數 \(O(1) \times (n^2 + \sum a_i)\)

Code
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 55;

int n, k;
int a[N], d[N];
vector<int> v[N];
vector<pair<int, int>> ans;

void Adjust () {
  int mx = 0;
  for (int i = 1; i <= n; ++i) {
    mx = max(mx, a[i] / k);
  }
  for (int i = 1; i <= n; ++i) {
    while (a[i] < mx * k) {
      ans.push_back({1, i});
      a[i] += k; 
    }
    a[i] -= mx * k;
  }
}

void Operate (int x, int v) {
  if (!v) return; 
  d[x] = (d[x] + v) % k; 
  d[x + k] = (d[x + k] - v + k) % k;
  for (int i = 1; i <= v; ++i) {
    ans.push_back({2, x});
  }  
  for (int i = 1; i <= n; ++i) {
    if (i < x || i >= x + k) {
      ans.push_back({1, i});
      ans.push_back({1, i});
      a[i] += k * 2 - v; 
    }
  }
  Adjust();
}

int main () {
  // freopen("tmp.in", "r", stdin);
  // freopen("tmp.out", "w", stdout);
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> k;
  for (int i = 1; i <= n; ++i) {
    cin >> a[i];
  }
  Adjust();
  for (int i = 1; i <= n + 1; ++i) {
    d[i] = (a[i] - a[i - 1] + k) % k;
    v[i % k].push_back(i);
  }
  for (int r = 0; r < k; ++r) {
    if (v[r].empty()) continue;
    int sum = 0; 
    for (auto i : v[r]) {
      sum = (sum + d[i]) % k; 
    }
    if (v[r][0] == 1) {
      for (int j = v[r].size() - 1; j; --j) {
        Operate(v[r][j - 1], d[v[r][j]]);
      }
    }
    else {
      if (sum && v[r].back() != n + 1) {
        cout << -1 << '\n';
        return 0; 
      }
      for (int j = 0; j < v[r].size() - 1; ++j) {
        int i = v[r][j];
        Operate(i, (k - d[i]) % k);
      }
    }
  }
  int mx = *max_element(a + 1, a + n + 1);
  for (int i = 1; i <= n; ++i) {
    int cnt = (mx - a[i]) / k; 
    for (int j = 1; j <= cnt; ++j) {
      ans.push_back({1, i});
    }
  }
  cout << ans.size() << '\n';
  for (auto i : ans) {
    cout << i.first << ' ' << i.second << '\n';
  }
  return 0; 
}

T2 Selling RNA Strands

題意:給定 \(n\) 個字串 \(s_{1 \sim n}\),有 \(m\) 次詢問,每次詢問格式如下:

  • 給出字串 \(p\)\(q\),求 \(s_{1 \sim n}\) 中有多少個字串同時以 \(p\) 為字首,並以 \(q\) 為字尾。

\(N = \max(n, m), S = \max(\sum|s_i|, \sum|p|, \sum|q|)\),保證 \(N \le 10^5, S \le 2 \times 10^6\),字符集大小為 \(4\)

首先考慮如果只有字首為 \(p\) 的限制是好做的,我們對於 \(s\)\(\text{Trie}\),每次查詢時走到 \(p\) 對應的 Trie 上的結點,做子樹標記求和即可。

現在加入字尾為 \(p\) 的限制,不難想到對 \(s\) 的反串再建一棵 \(\text{Trie}\),假設一個字串 \(s\) 在兩顆 Trie 上的對應點分別為 \(a\)\(b\),某次查詢的前字尾 \(p, q\) 在兩顆 \(\text{Trie}\) 上的對應點分別為 \(a_0\)\(b_0\),那麼 \(s\) 對該詢問有貢獻,當且僅當 \(a_0\)\(a\) 的祖先,\(b_0\)\(b_0\) 的祖先。

對於祖先的限制考慮用 \(\text{dfs}\) 序刻畫,那麼原問題可轉化成二維數點,掃描線 + 樹狀陣列即可,時間複雜度 \(O(S + N \log S)\)

Code
#include <iostream>
#include <algorithm>
#include <vector>
#include <numeric>

using namespace std;

const int N = 1e5 + 5, T = 2e6 + 5; 

int n, m; 
int a[N], b[N], p[N], ans[N];

string Read () {
  string s;
  cin >> s;
  for (auto &c : s)
    c = (c == 'A' ? 0 : (c == 'G' ? 1 : (c == 'U' ? 2 : 3)));
  return s;
}

struct Trie {
  int tot;
  int ch[T][4], dfn[T], siz[T];

  int Insert (string s) {
    int k = 0; 
    for (auto c : s) {
      if (!ch[k][c]) ch[k][c] = ++tot;
      k = ch[k][c];
    }
    return k; 
  }

  void Dfs (int x) {
    dfn[x] = ++tot;
    siz[x] = 1; 
    for (int i = 0, y; i < 4; ++i) {
      if (y = ch[x][i]) {
        Dfs(y);
        siz[x] += siz[y];
      }
    }
  }

  int Get_id (string s) {
    int k = 0; 
    for (auto c : s) {
      k = ch[k][c];
      if (!k) return -1;
    }
    return k;
  }
} trie, rtrie;

struct E {
  int x, l, r, v, id; 
};

struct Bit {
  int tr[T];

  void Add (int x, int y) {
    for (; x <= rtrie.tot; x += (x & -x)) {
      tr[x] += y;
    }
  }

  int Query (int l, int r) {
    int res = 0; 
    for (--l; l; l -= (l & -l)) {
      res -= tr[l];
    }
    for (; r; r -= (r & -r)) {
      res += tr[r];
    }
    return res;
  }
} bit;

int main () {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m;
  for (int i = 1; i <= n; ++i) {
    string s = Read();
    a[i] = trie.Insert(s);
    reverse(s.begin(), s.end());
    b[i] = rtrie.Insert(s);
  } 
  trie.Dfs(0), rtrie.Dfs(0);
  for (int i = 1; i <= n; ++i) {
    a[i] = trie.dfn[a[i]], b[i] = rtrie.dfn[b[i]];
  }
  vector<E> v;
  for (int i = 1; i <= m; ++i) {
    string s = Read(), t = Read();
    reverse(t.begin(), t.end());
    int p = trie.Get_id(s), q = rtrie.Get_id(t);
    if (p != -1 && q != -1) {
      int al = trie.dfn[p], ar = trie.dfn[p] + trie.siz[p] - 1, bl = rtrie.dfn[q], br = rtrie.dfn[q] + rtrie.siz[q] - 1; 
      v.push_back(E({al - 1, bl, br, -1, i}));
      v.push_back(E({ar, bl, br, 1, i}));
    }
  } 
  sort(v.begin(), v.end(), [&](E a, E b) -> bool { 
    return a.x < b.x;
  }); 
  iota(p + 1, p + n + 1, 1);
  sort(p + 1, p + n + 1, [&](int i, int j) -> bool {
    return a[i] < a[j];
  });
  int t = 1; 
  for (auto i : v) {
    while (t != n + 1 && a[p[t]] <= i.x) {
      bit.Add(b[p[t++]], 1);
    }
    ans[i.id] += bit.Query(i.l, i.r) * i.v;
  }
  for (int i = 1; i <= m; ++i) {
    cout << ans[i] << '\n';
  }
  return 0;  
}

T3 Skyscraper

題意:給定一個長度為 \(n\) 的序列 \(a_i\),滿足 \(a\) 中元素兩兩不相同。
給定 \(L\),計數有多少個 \(a\) 的排列 \(p\),滿足 \(\sum\limits_{i = 1}^{n - 1} |p_i - p_{i + 1}| \le L\)
\(1 \le n \le 10^2, 1 \le a_i, L \le 10^3\)

考慮 \(\sum\limits_{i = 1}^{n - 1} |p_i - p_{i + 1}| \le L\) 的限制,顯然正著是不好做的。套路的可以想到從上往下掃描線,維護連續段的數量。那麼我們的要求就是線段長度總和 \(\le L\)

假設我們從大往小加數,假設掃描線的上一個位置為 \(lst\),當前這個數所在位置為 \(cur\),那麼可能會有若干未閉合的插頭,每個會對線段長度總和造成 \(lst - cur\) 的貢獻。我們發現這樣的插頭數量至於當前的連續段數有關,於是我們並不關心當前加入的所有數的形態,而只需記錄有多少個連續段。

考慮一個問題,如果一個數在邊界,那麼它會少貢獻一個插頭,如右上圖所示。但是這個是好處理的,我們只需再記錄位於左右邊界上的數是否已經確定即可。

預設滾動陣列消去當且加的是第幾個數的維度。狀態就是 \(f_{i, j, p, q}\),表示當前有 \(i\) 個連續段,線段總長為 \(j\)\(p, q\) 表示是否欽定了最左和最右的數的方案數。

轉移有三種情況:新建立一個段、延續一個段、合併相鄰的兩個段,分類討論一下即可。

時間複雜度 \(O(n^2L)\)

Code
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 105, M = 1e3 + 5; 
const int Mod = 1e9 + 7; 

int n, lim;
int a[N], f[N][M][2][2], g[N][M][2][2];

void Add (int &x, int y) {
  x = ((x += y) >= Mod ? x - Mod : x);
}

int main () {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> lim;
  for (int i = 1; i <= n; ++i) {
    cin >> a[i];
  }
  sort(a + 1, a + n + 1, greater<int>());
  for (int i = 0; i < 2; ++i) 
    for (int j = 0; j < 2; ++j)
      f[1][0][i][j] = 1; 
  for (int i = 1; i < n; ++i) {
    for (int j = 1; j <= i; ++j) {
      for (int k = 0; k <= lim; ++k) {
        for (int p = 0; p < 2; ++p) {
          for (int q = 0; q < 2; ++q) {
            g[j][k][p][q] = f[j][k][p][q], f[j][k][p][q] = 0; 
          }
        }
      }
    }
    for (int j = 1; j <= i; ++j) {
      for (int k = 0; k <= lim; ++k) {
        for (int p = 0; p < 2; ++p) {
          for (int q = 0; q < 2; ++q) {
            int v = g[j][k][p][q];
            if (!v)
              continue;
            int _k = k + (j * 2 - p - q) * (a[i] - a[i + 1]);
            if (_k > lim) continue;
            Add(f[j + 1][_k][p][q], 1ll * (j - 1) * v % Mod);
            if (!p) {
              Add(f[j + 1][_k][0][q], v);
              Add(f[j + 1][_k][1][q], v);
            }
            if (!q) { 
              Add(f[j + 1][_k][p][0], v);
              Add(f[j + 1][_k][p][1], v);
            }
            Add(f[j][_k][p][q], 2ll * (j - 1) * v % Mod);
            if (!p) {
              Add(f[j][_k][0][q], v);
              Add(f[j][_k][1][q], v);
            }
            if (!q) {
              Add(f[j][_k][p][0], v);
              Add(f[j][_k][p][1], v);
            }
            if (j > 1) {
              Add(f[j - 1][_k][p][q], 1ll * (j - 1) * v % Mod);
            }
          }
        }
      }
    }
  }
  int ans = 0; 
  for (int i = 0; i <= lim; ++i) {
    ans = (ans + f[1][i][1][1]) % Mod;
  }
  cout << ans << '\n';
  return 0; 
}

相關文章