CF1946F Nobody is needed 题解

Nobody is needed

推销我的洛谷博客。

题意

多组数据。

给定一个长度为 \(n\)排列 \(a\),你需要回答 \(q\) 组询问,每组询问给出 \(l,r\),求有多少个子序列 \(t\) 使得:

  • \(l \leqslant t_1 < t_2 < \cdots < t_k \leqslant r\)
  • \(a_{t_i} | a_{t_{i + 1}}(1 \leqslant i < k)\)

数据范围

  • \(1 \leqslant n,q \leqslant 10^6\)
  • \(a\)一个排列
  • \(1 \leqslant \sum n,\sum q \leqslant 10^6\)

思路

考虑使用 dp,令 \(dp_i\) 表示以 \(a_i\) 结尾的合法子序列个数,拓扑序为 \(i\) 从小到大。

那么转移呢,很显然若是写收集形则 \(dp_i\) 可以从所有 \(a_i\) 的约数处转移过来,总复杂度是 \(O(\sum\limits_{1 \leqslant i \leqslant n}\sqrt{i})\) 的,可以用代码计算一下,当 \(n=10^6\) 时,总共需要大约 \(6 \times 10^8\),不太能接受。

若是写扩散形,又不便于确定对于每组询问的答案。

可以考虑将拓扑序转为 \(i\) 从大到小,那么对于 \(i\),会发生修改的 \(dp_j\) 只有那些 \(i \leqslant j\)\(a_i | a_j\),也就是说只有 \(\frac{n}{a_i}\) 处的 dp 需要修改,总复杂度为 \(O(\sum\limits_{1\leqslant i \leqslant n} \frac{n}{i})\),很典型的调和级数 \(O(n \log n)\),可以接受。

那么考虑怎么更新 dp 数组,正如上文所说,每次发生修改的只有 \(a_i\) 的倍数,而修改的贡献可以分为 \(a_i\)\(a_i \times x(x \geqslant 2)\) 的贡献和 \(a_i \times x(x \geqslant 2)\)\(a_i \times x \times y (x,y \geqslant 2)\) 的贡献,但如果 \(a_i\) 所对应的下标(即 \(i\))在 \(a_i \times x\) 的下标后面,则无法产生贡献,\(a_i \times x\)\(a_i \times x \times y\) 同理。

为了保证查询时答案没有超出 \([l,r]\) 的范围,需要在每次更新完 dp 数组后处理那些 \(l = i\) 的查询,答案就是 \(\sum\limits_{i\leqslant j \leqslant r} dp_j\),为了快速求出答案,使用树状数组维护 dp 数组的和。

详细见代码。

复杂度

  • 时间:\(O(q \log n + n \log^2 n)\)
  • 空间:\(O(n + q)\)

Code

点击查看代码
#include <iostream>
#include <vector>
#define _1 (__int128)1

using namespace std;
using ll = long long;

const int N = 1e6 + 10;

inline int lowbit (int x) {
  return x & -x;
}

int T, n, q, a[N], b[N];
ll ans[N], tr[N], dp[N];
vector<pair<int, int>> qry[N];

void modify (int x, int y) {
  for (int i = x; i <= n; i += lowbit(i)) tr[i] += y;
}

ll Query (int x) {
  ll ret = 0;
  for (int i = x; i; i -= lowbit(i)) ret += tr[i];
  return ret;
}

void Solve () {
  cin >> n >> q;
  for (int i = 1; i <= n; i++)
    cin >> a[i], b[a[i]] = i, tr[i] = 0, qry[i] = vector<pair<int, int>> (); // 多测要清空,b[i] 用于记录 i 在 a 中对应的下标
  for (int i = 1, l, r; i <= q; i++)
    cin >> l >> r, qry[l].push_back({r, i}); // 离线处理询问
  for (int i = n; i; i--) {
    dp[a[i]] = 1; // 初始状态
    for (int j = a[i]; j <= n; j += a[i]) 
      if (b[j] >= b[a[i]]) // 将两种转移合并可以这样写
        for (int k = j * 2; k <= n; k += j) // 注意是 k += j 而不是 k += a[i]
          if (b[k] > b[j])
            dp[k] += dp[j];
    for (int j = a[i]; j <= n; j += a[i])
      modify(b[j], dp[j]), dp[j] = 0; // 用树状数组维护 dp 和,dp 要清空
    for (auto [j, id] : qry[i])
      ans[id] = Query(j) - Query(i - 1); // 差分处理答案
  }
  for (int i = 1; i <= q; i++)
    cout << ans[i] << ' ';
}

int main () {
  ios::sync_with_stdio(0), cin.tie(0);
  // Solve();
  for (cin >> T; T--; Solve(), cout << '\n') {}
  return 0;
}
posted @ 2024-08-03 15:33  wnsyou  阅读(7)  评论(0编辑  收藏  举报