缺零分治

贪心 二分 前缀和

首先可以先对原序列进行处理。 肯定是要变成从 开始慢慢递增的(每次增加 ),后面的都可以不要了,反正放到哪个可重集都不会改变 的话,对于 满足 ,那么也要 ,否则 多的那一部分不可能会改变 的大小(直观理解就是 要连续递增, 要连续单调不增(增了就用前缀 解决))。

肯定是先要进行贪心的。对于 ,最简单的模拟方式是每次选择最大可能构造出来但是大小不超过 。显然 的值非常大,一步一步减肯定会 TLE。

最浅显的优化方式就是每次选择最大 之后直接选择多次,这样单次询问就可以做到 。多组询问仍然不好搞。考虑离线下来排序,用单调性一起做,但是发现使用了更高的 值所有更小的数量都要减掉,然而维护了这个就不能批量搞了(因为大家都不一样)。

所以考虑快速做减法的操作。维护后缀和 表示把 中所有 可以表示出的 都拿来用,能搞出的最大和。那么有递推式 表示拿 前缀能搞出来的 是因为 和后面的用了

这样,我们可以通过二分快速进行减法操作,找到最小的 满足 。现在的 后,已经无力减掉 的全部 了,但是可以减去部分。计算 并累加至答案当中。最后 都减不了了,如果还 的话,直接答案 即可。

时间复杂度

const LL kMaxN = 1e5 + 5;
 
LL a[kMaxN], b[kMaxN], sub[kMaxN], t, n, q;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> t; t; t--) {
    cin >> n >> q;
    for (LL i = 1; i <= n; i++) {
      cin >> a[i] >> b[i];
    }
    for (LL i = 1; i <= n; i++) {
      if (i == 1 && a[i] != 0 ||
          i != 1 && a[i] != a[i - 1] + 1) {
        n = i - 1;
        break;
      }
    }
    b[0] = 1e18;
    for (LL i = 1; i <= n; i++) { 
      b[i] = min(b[i - 1], b[i]);
    }
    b[n + 1] = sub[n + 1] = 0;
    for (LL i = n; i >= 1; i--) {
      sub[i] = sub[i + 1] + (a[i] + 1) * (b[i] - b[i + 1]);
    }
    for (LL m; q; q--) {
      cin >> m;
      if (n == 0) {
        cout << (m == 0 ? 1 : -1) << '\n';
        continue;
      }
      if (m == 0) {
        cout << "-1\n";
        continue;
      }
      if (m <= a[n] + 1) {
        cout << 1 + (m != a[n] + 1) << '\n';
        continue;
      }
      if (m - sub[1] > 0) {
        cout << "-1\n";
        continue;
      }
      LL p = -1;
      for (LL l = 1, r = n + 1; l <= r; ) {
        LL mid = (l + r) / 2;
        if (sub[mid] <= m) {
          p = mid;
          r = mid - 1;
        } else {
          l = mid + 1;
        }
      }
      LL use = b[p];
      m -= sub[p];
      if (m > 0) {
        use += m / (a[p - 1] + 1);
        m %= a[p - 1] + 1;
        if (m > 0) {
          use++;
        }
      }
      cout << use << '\n';
    }
  }
  return 0;
}

猜数游戏

背包dp 序列dp

  • [c] 赛时半个小时没读懂题,后面思考得过于复杂了,实际上很简单。 真是太有诱惑性了,小可爱出题人居然给时间复杂度 (而且还是标解)。

赛后发现就是 ZRR 题目。

首先看到数据范围就很像 DP。设 表示长度为 的子问题的答案(原问题是长度为 ,这里即 跳到 ),那么有 ,其中 表示跳一步长度为 的最小花费。直观来看,就是枚举 表示从 左边或右边掏掉 的,然后用 的代价补上。

求的方式可以通过完全背包搞。注意需要用到负数下标,所以需要设置偏移。时间复杂度

const LL kMaxN = 2e4 + 5, kM = 1e4;
 
LL a[kMaxN], b[kMaxN], wyy[kMaxN], zrr[kMaxN], c, t, n, m;
 
int main() {
  for (cin >> c >> t; t; t--) {
    cin >> n >> m;
    for (LL i = 1; i <= m; i++) {
      cin >> a[i];
    }
    for (LL i = 1; i <= m; i++) {
      cin >> b[i];
    }
    fill(begin(wyy), end(wyy), 1e18);
    fill(begin(zrr), end(zrr), 1e18);
    wyy[kM] = 0;
    for (LL i = 1; i <= m; i++) {
      for (LL j = a[i]; j <= 2 * kM; j++) {
        wyy[j] = min(wyy[j], wyy[j - a[i]] + b[i]);
      }
      for (LL j = 2 * kM - a[i]; j >= 0; j--) {
        wyy[j] = min(wyy[j], wyy[j + a[i]] + b[i]);
      }
    }
    zrr[1] = 0;
    for (LL i = 2; i <= n; i++) {
      for (LL j = 1; j < i; j++) {
        zrr[i] = min(zrr[i], max(zrr[j], zrr[i - j]) + wyy[kM + j]);
      }
    }
    cout << (zrr[n] == 1e18 ? -1 : zrr[n]) << '\n';
  }
  return 0;
}

树上求值

字典树 启发式合并

采用暴力。遍历每个节点 ,对 的节点对累加贡献。有两种可能的 存在方式:

  • 当中一个存在于另一个的子树当中。
  • 当中 分别存在于 的不同儿子的子树当中。

这些贡献是很好累加的。需要注意 的贡献,会被加两次,需要减掉。时间复杂度 ,成功获得 分。

LL calc(LL x) {
  LL res = 1;
  for (LL i = 0; i <= 20; i++) {
    res = res * a[i][x & 1] % mod;
    x >>= 1;
  }
  return res;
}
 
LL summary(LL lca, LL x, LL f) {
  LL res = val[x + d[lca]];
  for (LL i : e[x]) {
    if (i != f) {
      res = (res + summary(lca, i, x)) % mod;
    }
  }
  return res;
}
 
void add(LL x, LL f, LL s) {
  sum[x] = (sum[x] + s) % mod;
  for (LL i : e[x]) {
    if (i != f) {
      add(i, x, s);
    }
  }
}
 
void dfs(LL x, LL f) {
  d[x] = d[f] + 1;
  for (LL i : e[x]) {
    if (i != f) {
      dfs(i, x);
    }
  }
  add(x, f, val[x + d[x]]);
  sum[x] = (sum[x] + summary(x, x, f)) % mod;
  sum[x] = (sum[x] - val[x + d[x]] + mod) % mod;
  for (LL i : e[x]) {
    if (i == f) {
      continue;
    }
    LL s = summary(x, i, x);
    for (LL j : e[x]) {
      if (j == f || j == i) {
        continue;
      }
      add(j, x, s);
    }
  }
}

夜里亦始终想念着你

数学

我采用的暴力+特殊性质,r可以得到 分。

  • 对于 ,采用暴力 dfs 计算所有可能得到的最终局面,再做子集 DP 即可。时间复杂度
  • 对于保证奇数位置必有棋子的,偶数位置可以随便跳。对于最终的集合,我们枚举它有多少个选中了偶数位置,奇数位置就直接用 的幂算即可。需要配合前缀和处理。
void dfs(LL x) {
  // 记忆化搜索
}
 
LL dp() {
  // 子集 DP
}
 
f (n <= 20) {
  dfs(mask(s));
  cout << dp() << '\n';
  for (LL x; q; q--) {
    cin >> x;
    s[x] = ((s[x] - '0') ^ 1) + '0';
    fill(begin(f), end(f), 0);
    dfs(mask(s));
    cout << dp() << '\n';
  }
  return 0;
}
init();
LL wyy = 0, zrr = 0;
for (LL i = 1; i <= n; i++) {
  if (i % 2 == 0) {
    wyy++;
    cnt += s[i] == '1';
  } else {
    zrr++;
  }
}
ans[0] = 1;
for (LL i = 1; i <= wyy; i++) {
  ans[i] = (ans[i - 1] + comb(wyy, i)) % kMod;
}
LL base = pow(2, zrr);
cout << ans[cnt] * base % kMod << '\n';
for (LL x; q; q--) {
  cin >> x;
  s[x] = ((s[x] - '0') ^ 1) + '0';
  cnt -= s[x] == '0';
  cnt += s[x] == '1';
  cout << ans[cnt] * base % kMod << '\n';
}

总结

总分

T2 想得太复杂了,下次应该多思考不同的思路。