In mathematics you don't understand things, you just get used to them.

标签 data structures 下的文章

link。

兔兔弹性的因。

考虑一次修改产生的影响,求出前缀 lis 长度和后缀 lis 长度,然后一个一个一个一个。

struct seg_tree {
    int ms, mh;
    vi<int> md;
    seg_tree(int _n) {
        mh = ceil(log2(_n)), ms = 1<<mh;
        md = vi<int>(ms*2, 0);
    }
    void upd(int now) { md[now] = max(md[now*2], md[now*2+1]); }
    void mdf(int now, int val) {
        md[now += ms] = val;
        for (int i=1; i<=mh; ++i) upd(now>>i);
    }
    int qry(int pos) { return md[pos+ms]; }
    int qry(int low, int high) {
        int res = 0;
        for (low += ms, high += ms; low < high; low >>= 1, high >>= 1) {
            if (low&1) cmax(res, md[low++]);
            if (high&1) cmax(res, md[--high]);
        }
        return res;
    }
};
bool nes[1<<19];
int n, q, a[1<<19], lsh[1<<20], m, dp_pre[1<<19], dp_suf[1<<19];
int ans[1<<19], rec[1<<19], lis, cnt[1<<19];
vi<pii> req[1<<19];
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int x, y;
    cin >> n >> q;
    rep(i,n) cin >> a[i], lsh[m++] = a[i];
    rep(i,q) cin >> x >> y, req[--x].eb(lsh[m++] = y, i); 
    sort(lsh, lsh+m);
    m = unique(lsh, lsh+m)-lsh;
    rep(i,n) a[i] = lower_bound(lsh, lsh+m, a[i])-lsh;
    seg_tree pre(m), suf(m);
    rep(i,n) pre.mdf(a[i], dp_pre[i] = pre.qry(0, a[i])+1);
    drep(i,n) {
        rec[i] = suf.qry(a[i]);
        suf.mdf(a[i], dp_suf[i] = suf.qry(a[i]+1, m)+1);
    }
    rep(i,n) cmax(lis, dp_pre[i]+dp_suf[i]-1);
    rep(i,n) cnt[dp_pre[i]] += (dp_pre[i]+dp_suf[i]-1 == lis);
    rep(i,n) nes[i] = (dp_pre[i]+dp_suf[i]-1 == lis && cnt[dp_pre[i]] == 1);
    pre = seg_tree(m);
    rep(i,n) {
        suf.mdf(a[i], rec[i]);
        for (auto const& h : req[i]) {
            int to = lower_bound(lsh, lsh+m, h.first)-lsh;
            ans[h.second] = max(pre.qry(0, to)+suf.qry(to+1, m)+1, lis-nes[i]);
        }
        pre.mdf(a[i], dp_pre[i]);
    }
    copy(ans, ans+q, ostream_iterator<int>(cout, "\n"));
    cout << flush;
}

「codeforces - 1416D」Graph and Queries:显然倒着做,考虑怎么维护合并连通块。有个 kruskal 重构树的 trick 是,合并连通块时建个虚点把两个连通块的根连起来,这样就造出来了个树,查询都在子树,直接做。注意到这样做一个连通块不可能成为令一个连通块的子树,于是就不用启发式合并 dfn 了,直接记录当前合并操作时的根结点即可。

int n, m, q, fa[700100], a[700100], ldf[700100], rdf[700100], dfc, u[300100],
    v[300100];
bsi<int> adj[700100];
int ldr(int x) {
  while (x != fa[x]) x = fa[x] = fa[fa[x]];
  return x;
}
void dfs(int x) {
  ldf[x] = ++dfc;
  for (auto y : adj[x]) dfs(y);
  rdf[x] = dfc;
}
bitset<300100> tag;
void mrg(int u, int v) {
  u = ldr(u), v = ldr(v);
  if (u == v) return;
  n++, fa[n] = n, fa[u] = fa[v] = n;
  adj[n] += u, adj[n] += v;
}
int ms, mh;
vi<pii> md, qs;
signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> n >> m >> q;
  iota(fa + 1, fa + n + 1, 1);
  for (int i = 1; i <= n; ++i) cin >> a[i];
  for (int i = 1; i <= m; ++i) cin >> u[i] >> v[i];
  for (int t, x; q--;) {
    cin >> t >> x;
    qs.eb(t, x);
    if (t == 2) tag[x] = 1;
  }
  reverse(qs.begin(), qs.end());
  for (int i = 1; i <= m; ++i)
    if (!tag[i]) mrg(u[i], v[i]);
  for (auto& [t, x] : qs) {
    if (t == 2)
      mrg(u[x], v[x]);
    else
      x = ldr(x);
  }
  for (int i = 1; i <= n; ++i)
    if (ldr(i) == i) dfs(i);
  mh = ceil(log2(n)), ms = 1 << mh;
  md = vi<pii>(ms * 2, {0, 0});
  auto upd = [&](int now) { md[now] = max(md[now * 2], md[now * 2 + 1]); };
  auto mdf = [&](int now, const pii& w) {
    md[now += ms - 1] = w;
    for (int i = 1; i <= mh; ++i) upd(now >> i);
  };
  for (int i = 1; i <= n; ++i) md[ldf[i] + ms - 1] = pii(a[i], ldf[i]);
  for (int i = ms - 1; i >= 1; --i) upd(i);
  auto qry = [&](int l, int r) {
    pii res({0, 0});
    for (l += ms - 1, r += ms; l < r; l >>= 1, r >>= 1) {
      if (l & 1) cmax(res, md[l++]);
      if (r & 1) cmax(res, md[--r]);
    }
    return res;
  };
  reverse(qs.begin(), qs.end());
  for (auto const& [t, x] : qs) {
    if (t == 1) {
      auto ret = qry(ldf[x], rdf[x]);
      cout << ret.first << "\n";
      if (ret.first > 0) mdf(ret.second, {0, 0});
    }
  }
}

「codeforces - 1140F」Extending Set of Points:考虑二分图,张集大小就是二分图所有连通块左部大小乘右部大小(i.e. 连通块会被补全为一个「左右部内部没边的团」)。如果没有删除就可以直接用并查集维护左右部大小做了,加了删除考虑线段树分治和可撤销并查集即可。

这道题告诉了我们线段树分治的一点本质:可撤销并查集无非是拿个 stack 存操作,一次只能撤销一步,而线段树分治是一个,一个一个一个一个一个

const int n = 3e5;
int q, sl[600100], sr[600100], fa[600100], siz[600100];
ll ans;
mli mp;
struct state {
  int x, y, sl, sr, siz;
  ll res;
} stk[600100];
state* top = stk;
vi<pii> md[1200100];
int ldr(int x) {
  while (x != fa[x]) x = fa[fa[x]];
  return x;
}
void mrg(int x, int y) {
  x = ldr(x), y = ldr(y);
  if (x == y) return;
  if (siz[x] < siz[y]) swap(x, y);
  *++top = {x, y, sl[x], sr[x], siz[x], ans};
  ans -= 1ll * sl[x] * sr[x] + 1ll * sl[y] * sr[y] -
         1ll * (sl[x] + sl[y]) * (sr[x] + sr[y]);
  sl[x] += sl[y], sr[x] += sr[y], siz[x] += siz[x] == siz[y], fa[y] = x;
}
#define mid ((l + r) / 2)
void mdf(int ql, int qr, const pii& w, int now, int l, int r) {
  if (l > qr || r < ql) return;
  if (l >= ql && r <= qr) return md[now].pb(w);
  mdf(ql, qr, w, now * 2, l, mid), mdf(ql, qr, w, now * 2 + 1, mid + 1, r);
}
void rollback() {
  assert(top != stk);
  fa[top->y] = top->y, siz[top->x] = top->siz, sl[top->x] = top->sl,
  sr[top->x] = top->sr, ans = top->res;
  top--;
}
void solve(int now, int l, int r) {
  int tmp = top - stk;
  for (auto const& [x, y] : md[now]) mrg(x, y);
  if (l == r)
    cout << ans << " \n"[r == q];
  else
    solve(now * 2, l, mid), solve(now * 2 + 1, mid + 1, r);
  while (top - stk > tmp) rollback();
}
signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0);
  cin >> q;
  for (int i = 1, x, y; i <= q; ++i) {
    cin >> x >> y;
    if (mp.count(1ll * (x - 1) * n + y))
      mdf(mp[1ll * (x - 1) * n + y], i - 1, {x, y + n}, 1, 1, q),
          mp.erase(1ll * (x - 1) * n + y);
    else
      mp[1ll * (x - 1) * n + y] = i;
  }
  for (auto const& [x, y] : mp)
    mdf(y, q, {(x + n - 1) / n, (x % n == 0 ? n : x % n) + n}, 1, 1, q);
  iota(fa + 1, fa + n * 2 + 1, 1);
  for (int i = 1; i <= n; ++i) sl[i] = 1;
  for (int i = n + 1; i <= n + n; ++i) sr[i] = 1;
  solve(1, 1, q);
}

link。

给一种不一样的写法,避开了常数较大的函数式字典树 并获得了更大的其他常数

考虑 Borůvka 的过程:每次找到一个连通块到其他连通块最小的出边并合并连通块,复杂度分析同启发式合并。我们只需要考虑怎么求「一个连通块到外部的最小出边」即可。

我们维护一棵全局的字典树,在查询连通块 $S$ 的最小出边时删除 $S$ 中的点后查询异或最小值即可。

对于字典树的删除操作,我们可以对于字典树上的结点记录 parent node,然后删除的时候从叶子开始跳 parent 打删除标记即可。

注意删除操作必须通过打标记来实现而不是直接将后继结点的指针清空,因为这样会带来额外一个 $O(\log)$ 的空间开销。还有些细节参考给出的实现。

int n, a[200100], _tot, pos[200100], bits;
struct node {
  int fa, son[2], cnt, id;
  bool del[2];
} t[6000100];
void ins(int val, int id) {
  int now = 0;
  for (int i = bits; i >= 0; --i) {
    if (!t[now].son[(val>>i)&1]) t[now].son[(val>>i)&1] = ++_tot;
    if (t[now].del[(val>>i)&1]) t[now].del[(val>>i)&1] = 0;
    t[t[now].son[(val>>i)&1]].fa = now, t[now].cnt++;
    now = t[now].son[(val>>i)&1];
  }
  t[now].id = id, t[now].cnt++, pos[id] = now;
}
void del(int id) {
  for (int now = pos[id]; now; now = t[now].fa) {
    if (--t[now].cnt == 0) t[t[now].fa].del[t[t[now].fa].son[1] == now] = 1;
  }
}
int Id;
int qry(int val) {
  int res = 0, now = 0;
  for (int i = bits; i >= 0; --i) {
    if (t[now].son[(val>>i)&1] && !t[now].del[(val>>i)&1]) now = t[now].son[(val>>i)&1];
    else {
      res += 1<<i, now = t[now].son[~(val>>i)&1];
    }
  }
  Id = t[now].id;
  return res;
}
int ust[200100];
int ldr(int x) {
  while (x != ust[x]) x = ust[x] = ust[ust[x]];
  return x;
}
int rec[200100], _min[200100], to[200100];
bsi<int> cc[200100];
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; ++i) {
    cin >> a[i], cmax(bits, (int)ceil(log2(a[i])));
  }
  sort(a+1, a+n+1);
  n = unique(a+1, a+n+1)-a-1;
  for (int i = 1; i <= n; ++i) ins(a[i], i);
  iota(ust+1, ust+n+1, 1);
  ll ans = 0;
  for (int num = n; num > 1;) {
    int tot = 0;
    for (int i = 1; i <= n; ++i) {
      if (cc[ldr(i)].empty()) rec[++tot] = ldr(i);
      cc[ldr(i)] += i;
    }
    for (int i = 1; i <= tot; ++i) _min[i] = to[i] = -1;
    for (int i = 1; i <= tot; ++i) {
      for (auto id : cc[rec[i]]) del(id);
      for (auto id : cc[rec[i]]) {
        int ret = qry(a[id]);
        if (_min[i] == -1 || _min[i] > ret) _min[i] = ret, to[i] = Id;
      }
      for (auto id : cc[rec[i]]) ins(a[id], id);
    }
    for (int i = 1; i <= tot; ++i) {
      if (ldr(to[i]) != rec[i]) ans += _min[i], ust[rec[i]] = ldr(to[i]), num--;
    }
    for (int i = 1; i <= tot; ++i) bsi<int>().swap(cc[rec[i]]);
  }
  cout << ans << "\n";
}

link。

好题啊。

首先有一个类 kruskal 暴力,就是对于每一个询问,把所有边按权值大小排降序,第一个加进去成为奇环的边就是答案。注意我们不需要关注偶环长成什么样子,所以我们实际上维护的是一棵生成树。这个可以用并查集维护结点到根的边的数量来实现。

因此我们需要关注的边只有 $O(n)$ 条了(生成树),那么维护一个区间的需要关注的边的边集,具体而言就是树边和奇环上的边。考虑用线段树,合并的时候用归并即可。

int n, m, q, fa[2100], dst[2100], ms, mh;
int u[1000100], v[1000100], w[1000100];
vi<bsi<int>> md;
bool flag;
int ldr(int x) {
    if (x != fa[x]) {
        int u = ldr(fa[x]);
        dst[x] ^= dst[fa[x]];
        return fa[x] = u;
    }
    return x;
}
int mrg(int x, int y) {
    int u = ldr(x), v = ldr(y);
    if (u == v) {
        if (dst[x] == dst[y]) return 2;
        return 0;
    }
    fa[v] = u;
    dst[v] = dst[x]^dst[y]^1;
    return 1;
}
bsi<int> mrg(const bsi<int>& lhs, const bsi<int>& rhs) {
    bsi<int> qwq, res;
    merge(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), back_inserter(qwq), [&](int x, int y) {
        return w[x] > w[y];
    });
    for (auto i : qwq) {
        fa[u[i]] = u[i], fa[v[i]] = v[i];
        dst[u[i]] = dst[v[i]] = 0;
    }
    for (auto i : qwq) {
        int tmp = mrg(u[i], v[i]);
        if (tmp) res += i;
        if (tmp == 2) {
            flag = 1;
            break;
        }
    }
    return res;
}
void upd(int now) {
    md[now] = mrg(md[now*2], md[now*2+1]);
}
int qry(int l, int r) {
    bsi<int> res;
    flag = 0;
    for (l += ms-1, r += ms; l < r; l >>= 1, r >>= 1) {
        if (l&1) res = mrg(res, md[l++]);
        if (r&1) res = mrg(md[--r], res);
    }
    if (flag) return w[res.back()];
    return -1;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m >> q;
    mh = ceil(log2(m)), ms = 1<<mh;
    md = vi<bsi<int>>(ms*2, bsi<int>());
    for (int i=1; i<=m; ++i) {
        cin >> u[i] >> v[i] >> w[i];
    }
    for (int i=1; i<=m; ++i) {
        md[i+ms-1] = bsi<int>({i});
    }
    for (int i=ms-1; i>=1; --i) {
        upd(i);
    }
    for (int l,r; q--;) {
        cin >> l >> r;
        cout << qry(l, r) << "\n";
    }
}

link。

平时基本打不到 ex,这个 ex 还是比较 ez 的,但也有些需要注意的地方。

考虑 dp 规划前缀,设 $f[i][0/1]$ 表示前缀 $[1, i]$ 否是选 $i$ 的方案数,这个明显会算重,注意到前缀 $[1, i)$ 凑不出来的字符串可以通过前缀 $[1, i)$ 再 append 一个 $0/1$ 凑出来,所以我们改描述 $f[i][0/1]$ 为前缀 $[1, i]$ 凑出结尾为 $\texttt0/\texttt1$ 的方案数。

转移分 $s_i$ 是 $\texttt{0}/\texttt{1}/\texttt{?}$ 讨论,放到矩阵上做 ddp 即可,如果令 $f[0][0] = f[0][1] := 1$ 可以缩减矩阵的规模至 $2$,但是这样答案就要减 $2$。

这类 dp 通常对当前元素的直接规划会算重,可以将目光放到已经构造出的结果上,再结合结论(其实比较 ad hoc 了)避免算重。

// s[i]=0: dp[i][0] = dp[i-1][0]+dp[i-1][1], dp[i][1] = dp[i-1][1]
// s[i]=1: dp[i][0] = dp[i-1][0], dp[i][1] = dp[i-1][0]+dp[i-1][1]
// s[i]=?: dp[i][0/1] = dp[i-1][0]+dp[i-1][1]
using modint = modint998244353;
using mt = static_matrix<modint, 2>;
int n, q, ms, mh;
char s[100100];
vi<mt> md;
mt get(int i) {
    return mt({{1, s[i-1] == '1' || s[i-1] == '?'}, {s[i-1] == '0' || s[i-1] == '?', 1}});
}
void upd(int now) {
    md[now] = md[now*2]*md[now*2+1];
}
void mdf(int now, const mt& w) {
    md[now += ms-1] = w;
    for (int i=1; i<=mh; ++i) {
        upd(now>>i);
    }
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> q >> s;
    mh = ceil(log2(n)), ms = 1<<mh;
    md = vi<mt>(ms*2, mt::id());
    for (int i=1; i<=n; ++i) {
        md[i+ms-1] = get(i);
    }
    for (int i=ms-1; i>=1; --i) {
        upd(i);
    }
    char c;
    for (int x; q--;) {
        cin >> x >> c;
        s[x-1] = c;
        mdf(x, get(x));
        mt ret = mt({{1, 1}, {0, 0}})*md[1];
        cout << (ret[0][0]+ret[0][1]-2).val() << "\n";
    }
}