GDKOI 2021 Day 2

QOJ 7366 游戏

首先写出 \(1\le i < n\) 的转移式 \(E_i = p_iE_{i + 1} + (1 - p_{i})E_{i - 1}\),和 \(i = 0\) 的转移式 \(E_0 = p_0E_1 + (1 - p_0)E_0\)
那么根据 \(1\le i< n\) 的式子,就可以得到 \(E_{i + 1}\) 关于 \(E_{i}, E_{i - 1}\) 的递推式,根据 \(i = 0\) 的式子可以得到 \(E_1\)\(E_0\) 的关系。
同时发现这些转移式子都是形如乘上一些常数再相加还要加一些常数,于是可以表示为 \(E_i = a_i E_1 + b_i\),就可以递推 \(a_i, b_i\) 了。
最后根据 \(E_n = 0\) 可以解出 \(E_1\)

时间复杂度 \(\mathcal{O}(n\log \operatorname{mod})\),瓶颈在逆元,可以离线下来做到 \(\mathcal{O}(n + \log \operatorname{mod})\)

代码
#include<bits/stdc++.h>
using ll = long long;
constexpr ll mod = 998244353;
inline ll qpow(ll a, ll b, ll v = 1) {
   for (; b; b >>= 1, a = a * a % mod) {
      if (b & 1) v = v * a % mod;
   }
   return v;
}
inline ll inv(ll a) { return qpow(a, mod - 2); }
constexpr int maxn = 1e6 + 10; 
ll p[maxn], a[maxn], b[maxn];
int main() {
   int n; scanf("%d", &n);
   for (int i = 0, x, y; i < n; i++) {
      scanf("%d%d", &x, &y), p[i] = 1ll * x * inv(y) % mod;
   }
   a[0] = 1ll, a[1] = 1ll, b[1] = mod - inv(p[0]);
   for (int i = 1; i < n; i++) {
      ll invp = inv(p[i]);
      a[i + 1] = (a[i] - (1ll - p[i] + mod) * a[i - 1] % mod + mod) * invp % mod;
      b[i + 1] = (b[i] - (1ll - p[i] + mod) * b[i - 1] % mod - 1ll + mod + mod) * invp % mod; 
   }
   printf("%lld\n", (mod - b[n]) * inv(a[n]) % mod);
   return 0;
}

QOJ 7367 群岛

考虑分析一下根据当前的连边,\(u\) 到底可以走到哪里。
首先贪心的,因为一定有 \(i\to i + 1\) 的连边,所以 \(u\) 可以走到 \(v\ge u\) 的任意一个 \(v\)
那么如果存在 \(a_v < u\),走 \(u\to v\to a_v\) 一定更优,因为此时 \(a_v\) 的决策肯定包含了 \(u\) 的决策。

于是整个过程会直到不存在 \(v\ge u\) 使得 \(a_v < u\) 停止。
所以说对于最后的答案,设为 \(p\),一定满足对于 \(p < u'\le u\),一定存在 \(v\ge u'\) 使得 \(a_{v} < u'\);而对于 \(p\),对于所有 \(v\ge p\),一定 \(a_v\ge p\)

\(p < u'\le u\) 是合法的,\(p\) 是不合法的,于是考虑刻画这个合法的形式。
但是对于每个 \(u\) 都看是否存在 \(v\) 就不能很好的维护,于是考虑分析 \(v\) 能够影响到的 \(u\)。根据分析过程可以知道这些 \(u\) 满足 \(a_v < u\le v\)
于是可以考虑转化为 \((a_v, v]\) 区间 \(+1\),那么值 \(> 0\) 就是合法的,\(= 0\) 就是不合法的。

于是可以用线段树区间加维护修改,对于询问考虑线段树二分第一个 \(\le u\) 且值为 \(0\) 的位置,时间复杂度 \(\mathcal{O}(n\log n)\)

代码
#include<bits/stdc++.h>
constexpr int maxn = 1e5 + 10;
int n, m, a[maxn];
int mn[maxn * 4], tag[maxn * 4];
inline void pushup(int k) { mn[k] = std::min(mn[k << 1], mn[k << 1 | 1]); }
inline void pushtag(int k, int x) { mn[k] += x, tag[k] += x; }
inline void pushdown(int k) {
   if (! tag[k]) return ;
   pushtag(k << 1, tag[k]), pushtag(k << 1 | 1, tag[k]), tag[k] = 0;
}
inline void update(int x, int y, int z, int k = 1, int l = 1, int r = n) {
   if (x <= l && r <= y) return pushtag(k, z), void();
   pushdown(k);
   int mid = l + r >> 1;
   if (x <= mid) update(x, y, z, k << 1, l, mid);
   if (mid < y) update(x, y, z, k << 1 | 1, mid + 1, r);
   pushup(k);
}
inline int query(int R, int k = 1, int l = 1, int r = n) {
   if (r <= R) {
      if (mn[k] > 0) return -1;
      if (l == r) return l;
   }
   pushdown(k);
   int mid = l + r >> 1; int ans = -1;
   if (mid < R) ans = query(R, k << 1 | 1, mid + 1, r);
   if (ans == -1) ans = query(R, k << 1, l, mid);
   return ans;
}
int main() {
   scanf("%d%d", &n, &m);
   for (int i = 1; i <= n; i++) {
      scanf("%d", &a[i]);
      if (a[i] < i) update(a[i] + 1, i, 1);
   }
   for (int op, x, y; m--; ) {
      scanf("%d%d", &op, &x);
      if (op == 2) {
         printf("%d\n", query(x));
      } else {
         scanf("%d", &y);
         if (a[x] < x) update(a[x] + 1, x, -1);
         a[x] = y;
         if (a[x] < x) update(a[x] + 1, x, 1);
      }
   }
   return 0;
}

QOJ 7368 抄写

又来 manacher?

两个操作尤其是这个操作二其实提示的很明显,这个固定中心的想法,其实就是 manacher。
于是可以考虑给 manacher 的占位符赋予一个 \(0\) 的权值,这样就只剩下操作二了。

接下来对于这个答案,因为这个形式就是翻折和扩展,必定会使长度增加,并且不会再减少,于是可以考虑 dp,设 \(f_i\) 为匹配上 \([1, i]\) 的答案。
那么一个转移就是直接扩展,\(f_i = f_{i - 1} + cost_{s_i}\)
还有一个转移是翻折,那么令这个回文串是 \([l, mid, r]\) 的形式,其中 \(s_{l\sim r}\) 是回文串,\(mid\) 是中心,那么有转移 \(f_r = f_{mid} + C\)

此时肯定会贪心的找到最小的 \(f_{mid}\) 转移给 \(f_r\)
因为 manacher 顺带也可以求出 \(mid\) 可以转移的范围 \([mid, mid + p_{mid}]\),于是可以直接用一个单调队列线性做完。

不想写 manacher,有没有更好写的做法阿?

但是实际上感受一下也能感觉到,\(f_i\ge f_{i - 1}\),也即 \(f_i\) 不减。
证明考虑两种转移:如果是扩展,那么因为 \(cost_c\ge 0\),一定有 \(f_i\ge f_{i - 1}\);如果是翻折,对于 \([j, mid, i]\) 是个合法的回文串,那么 \([j + 1, mid, i - 1]\) 也一定是回文串,所以也有 \(f_i\ge f_{i - 1}\)

于是对于一个 \(r\),一定会贪心的选择最小的 \(mid\),那么 \(mid\) 要取到最小,\(l\) 一定就会取到最小。(这个对于上述 manacher 的单调队列也适用,只不过没啥变化。)
于是可以直接 用 hash 维护对于 \(r\) 其最远的回文串左端点,非常好写。
不过还是建议像 manacher 那样用占位符变到只有操作二,不用分讨。

两种方法的时间复杂度就是 \(\mathcal{O}(n)\)

manacher 做法代码
#include<bits/stdc++.h>
using ll = long long;
constexpr int maxn = 2e6 + 10;
char s_[maxn];
int n, s[maxn];
ll c[27], C, f[maxn];
int mr[maxn], q[maxn], hd = 1, tl = 0;
int main() {
   scanf("%d%lld", &n, &C);
   for (int i = 1; i <= 26; i++) scanf("%lld", &c[i]);
   scanf("%s", s_ + 1);
   for (int i = 1; i <= n; i++) {
      s[i * 2] = s_[i] - 'a' + 1;
   }
   s[0] = -1, s[n * 2 + 2] = -2;
   memset(f, 0x3f, sizeof(f)), f[0] = 0;
   for (int i = 1; i <= n * 2 + 1; i++) {
      f[i] = f[i - 1] + c[s[i]];
      if (hd <= tl && q[hd] + mr[q[hd]] < i) hd++;
      if (hd <= tl) {
         f[i] = std::min(f[i], f[q[hd]] + C);
         mr[i] = std::min(q[tl] + mr[q[tl]] - i, mr[q[tl] * 2 - i]);
      }
      while (s[i - mr[i] - 1] == s[i + mr[i] + 1]) mr[i]++;
      if (i + mr[i] > q[tl] + mr[q[tl]]) q[++tl] = i; 
   }
   printf("%lld\n", f[n * 2 + 1]);
   return 0;
}
hash 做法代码

暂时咕咕了,会有的 qaq

QOJ 7369 堆

考虑先分析一些特征值。
比如说手玩的过程中至少能够知道,每一次 down 后,当前节点的值一定是子树中的最小值,可以归纳证明。
于是可以至少知道,到最后根节点一定是子树中的最小值。

于是就可以考虑成一次性直接把子树最小值“提”到根节点,而经过路径上的节点就会沿着这个路径往下掉一个位置。
那么此时根节点就相当于是被固定了,那么不会再进行操作,那么就可以直接删掉这个节点,整个问题就变成了至多两个更小的堆了。

于是接下来考虑对于这些小的堆又怎么处理。
此时会发现,不在上述路径上的点的 down 操作肯定最后还是子树的最小值;如果在路径上,那么会按照刚刚说的,先往下掉一个位置,但是再往后能够发现,后面进行的 down 操作一样会把当前点的子树的最小值换上来。
于是会发现,此时依然可以像第一步那样子考虑,也即是把最小值拎上来,经过的点就往下掉一个位置。

于是考虑直接来维护这个过程。
对此,应该要知道子树的最小值和其所在位置,因为会有下落操作,所以还需要维护一下掉下来的这些点的权值。
进一步,发现其实很多信息是根本不关心的,这是因为对于询问来说,从根开始,一定是做完这个节点的操作后就只会递归到其中一个子树。
于是可以考虑只维护下当前节点的信息和在过程中被修改(经历了下落)的结点的信息。

那么这样的点数就只有 \(\mathcal{O}(\log^2 n)\) 个(进行 \(\log n\) 次操作,每次有 \(\log n\) 个节点下落)。
下落的权值改变可以用一个类似 pushup 来维护,可以做到 \(\mathcal{O}(\log^2 n)\)
主要瓶颈在于每次新开一个节点时获取子树最小值信息我暂且只会预处理 st 表出来对于 \(\log n\) 层分别求 \(\max\)

时间复杂度 \(\mathcal{O}(n\log n + q\log^3 n)\)
好像没有卡常就跑过去了,不慢。但是我认为正解应该对于那个预处理可以 \(\mathcal{O}(1)\) 做,如果有人会希望能教教我 qaq。

写的可能有点模糊了,可以结合一下代码。

代码
#include<bits/extc++.h>
constexpr int maxn = 1e5 + 10;
int n, a[maxn], b[maxn];
std::pair<int, int> mn[18][maxn];
inline std::pair<int, int> query(int l, int r) {
   if (l > r) return {n + 1, 0};
   int k = std::__lg(r - l + 1);
   return std::min(mn[k][l], mn[k][r - (1 << k) + 1]);
}
inline std::pair<int, int> query_t(int x, int m, int l) {
   std::pair<int, int> ans = {n + 1, 0};
   for (int y = x; x <= m; x = x * 2, y = std::min(y * 2 + 1, m)) {
      ans = std::min(ans, query(x + l - 1, y + l - 1));
   }
   return ans;
}
bool vis[maxn * 2]; int val[maxn * 2];
std::pair<int, int> rt[maxn * 2];
std::vector<int> ins;
inline void query1() {
   const std::pair<int, int> inf = {n + 1, 0};
   int l, m, id;
   scanf("%d%d%d", &l, &m, &id), m = m - l + 1;
   ins.clear();
   auto pushup = [&](int x) {
      rt[x] = std::min({rt[x * 2], rt[x * 2 + 1], {val[x], x + l - 1}});
   };
   auto add = [&](int x) {
      vis[x] = true, ins.push_back(x), val[x] = a[x + l - 1];
      if (rt[x * 2] == inf) rt[x * 2] = query_t(x * 2, m, l), ins.push_back(x * 2);
      if (rt[x * 2 + 1] == inf) rt[x * 2 + 1] = query_t(x * 2 + 1, m, l), ins.push_back(x * 2 + 1);
      pushup(x);
   };
   std::vector<int> rt_u;
   for (int x = id / 2; x >= 1; x /= 2) rt_u.push_back(x);
   std::reverse(rt_u.begin(), rt_u.end());
   for (int u : rt_u) {
      if (! vis[u]) add(u);
      int v = rt[u].second;
      std::vector<int> add_u;
      for (int x = v - l + 1; ! vis[x]; x /= 2) add(x);
      for (int x = v - l + 1; x != u; x /= 2) {
         std::swap(val[x], val[x / 2]);
         pushup(x);
      }
   }
   if (! vis[id]) add(id);
   printf("%d\n", rt[id].first);
   for (int x : ins) vis[x] = 0, rt[x] = inf;
}
inline void query2() {
   const std::pair<int, int> inf = {n + 1, 0};
   int l, m, id;
   scanf("%d%d%d", &l, &m, &id), m = m - l + 1, id = b[id] - l + 1;
   ins.clear();
   auto pushup = [&](int x) {
      rt[x] = std::min({rt[x * 2], rt[x * 2 + 1], {val[x], x + l - 1}});
   };
   auto add = [&](int x) {
      vis[x] = true, ins.push_back(x), val[x] = a[x + l - 1];
      if (rt[x * 2] == inf) rt[x * 2] = query_t(x * 2, m, l), ins.push_back(x * 2);
      if (rt[x * 2 + 1] == inf) rt[x * 2 + 1] = query_t(x * 2 + 1, m, l), ins.push_back(x * 2 + 1);
      pushup(x);
   };
   for (int w = 0; ; w++) {
      int u = 0;
      for (int c = 0; c <= w; c++) u = u * 2 + (id >> std::__lg(id) - c & 1);
      if (! vis[u]) add(u);
      int v = rt[u].second;
      if (v == id + l - 1) {
         printf("%d\n", u); break;
      }
      std::vector<int> add_u;
      for (int x = v - l + 1; ! vis[x]; x /= 2) add(x);
      for (int x = v - l + 1; x != u; x /= 2) {
         if (id == x / 2) id = x;
         std::swap(val[x], val[x / 2]);
         pushup(x);
      }
   }
   for (int x : ins) vis[x] = 0, rt[x] = inf;
}
int main() {
   int q; scanf("%d%d", &n, &q);
   a[0] = n + 1;
   for (int i = 1; i <= n; i++) {
      scanf("%d", &a[i]), b[a[i]] = i, mn[0][i] = {a[i], i};
   }
   for (int i = 1; i < 18; i++) {
      for (int l = 1, r = 1 << i; r <= n; l++, r++) {
         mn[i][l] = min(mn[i - 1][l], mn[i - 1][l + (1 << i - 1)]);
      }
   }
   for (int i = 1; i <= n * 2 + 1; i++) rt[i] = {n + 1, 0};
   for (int op; q--; ) {
      scanf("%d", &op);
      if (op == 1) query1();
      if (op == 2) query2();
   }
   return 0;
}
posted @ 2025-03-31 22:24  rizynvu  阅读(54)  评论(0)    收藏  举报