[P9167] [省选联考 2023] 城市建造

Posted JCY_std

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[P9167] [省选联考 2023] 城市建造相关的知识,希望对你有一定的参考价值。

洛谷题面

原题等价于计算有多少个点集 \\(V\\),满足删去 \\(V\\) 的导出子图中的边后,原图形成了 \\(|V|\\) 个连通块,且连通块大小的极差 \\(\\le k\\)。形成 \\(|V|\\) 个连通块又等价于 \\(V\\) 中的每个点都分属不同的连通块,我们称这样的 \\(V\\)合法的。

考虑在同一个点双连通分量中的三点 \\(u, v, w\\)。若 \\(u, v \\in V\\)\\(w \\not \\in V\\),根据点双连通的性质,我们可以找到一条从 \\(u\\)\\(v\\) 经过 \\(w\\) 的简单路径,此时必有两个 \\(V\\) 中的点属于同一个连通块。因此对于合法\\(V\\),同一个点双连通分量中的点,要么全在 \\(V\\) 中,要么只有一个在 \\(V\\) 中,要么全不在 \\(V\\) 中。

我们还可以发现,若 \\(V\\) 的导出子图不连通,则必有两个 \\(V\\) 中的点属于同一个连通块。因此对于合法\\(V\\),其导出子图必须连通。

上面两条性质启发我们建出原图的圆方树。不难发现,我们选择的 \\(V\\) 一定是在圆方树上“连通”的一些点双连通分量。具体地,用一个方点在 \\(S\\) 中代表该点双连通分量中的所有点都在 \\(V\\) 中,合法\\(V\\) 对应的 \\(S\\) 需要满足:若方点 \\(u, v \\in S\\),则所有在圆方树上 \\(u, v\\) 路径上的方点都在 \\(S\\) 中。我们称这样的 \\(S\\)合法的。

删去 \\(V\\) 的导出子图中的边后原图的连通块,和删去 \\(S\\) 中的方点后圆方树的连通块一一对应。定义圆方树上一个连通块的大小为连通块的圆点数,问题转化为计算有多少个圆方树上的合法方点点集 \\(S\\),满足删去 \\(S\\) 中的点后连通块大小的极差 \\(\\le k\\)。我们称这样的 \\(S\\)符合题意的。

考虑 \\(k \\le 1\\) 有什么用。不难猜想 \\(S\\) 一定包含重心,因为连通块的划分看起来十分平均。

证明:

令圆点的点权为 \\(1\\),方点的点权为 \\(0\\),找出圆方树的带权重心 \\(rt\\)\\(rt\\) 的所有儿子子树内的点权和都 \\(\\le \\fracn2\\)

如果 \\(rt\\) 为方点,则符合题意\\(S\\) 必然包含 \\(rt\\),因为若不包含则 \\(rt\\) 所在的连通块点权和 \\(\\ge \\fracn2 + 1\\),而 \\(rt\\) 不在的连通块点权和都 \\(\\le \\fracn2 - 1\\),无论 \\(k\\) 取多少都不符合题意

如果 \\(rt\\) 为圆点,对于符合题意\\(S\\)\\(\\forall u \\in S\\),以 \\(rt\\) 为根时 \\(u\\) 的所有祖先方点都应属于 \\(S\\),证明同理。

证毕。

\\(rt\\) 为方点时以 \\(rt\\) 的任意相邻圆点为根,当 \\(rt\\) 为圆点时以 \\(rt\\) 为根,将圆方树定根。这样,对于符合题意\\(S\\)\\(\\forall u \\in S\\)\\(u\\) 的所有祖先方点都应属于 \\(S\\)

先考虑 \\(k = 0\\) 时怎么做。

枚举连通块大小 \\(x\\),记 \\(sz_i\\) 表示圆方树定根后 \\(i\\) 的子树内的权值和。对于方点 \\(u\\),可以发现其是否属于 \\(S\\) 可以直接通过 \\(sz_u\\) 确定。若 \\(sz_u < x\\)\\(u \\not \\in S\\)。若 \\(sz_u > x\\)\\(u \\in S\\)。若 \\(sz_u = x\\),如果定根后 \\(u\\) 的儿子数 \\(\\ge 2\\)\\(u \\not \\in S\\),否则 \\(u \\in S\\)。证明考虑反证,若不满足上述任一条件则 \\(S\\) 必然不符合题意,读者可自证。

因此可以直接从小到大枚举 \\(x\\),用并查集维护连通性,再开一个桶 \\(cnt_i\\) 记录连通块大小为 \\(i\\) 的连通块数量即可。

再考虑 \\(k = 1\\)

容易想到一个容斥,枚举连通块大小的集合 \\(\\x, x + 1\\\\),钦定所有连通块大小属于该集合,计算方案数,然后减去连通块大小全为 \\(x\\) 或全为 \\(x + 1\\) 的方案数。类似 \\(k = 0\\),若 \\(sz_u < x\\)\\(u \\not \\in S\\)。若 \\(sz_u > S\\)\\(u \\in S\\)。若 \\(sz_u = x\\),如果定根后 \\(u\\) 的儿子数 \\(\\ge 2\\)\\(u \\not \\in S\\),否则特殊处理。

特殊处理是 \\(k = 1\\) 的一个小难点。

先考虑 \\(x > 1\\) 的情况。

\\(fa_i\\) 为定根后点 \\(i\\) 的父亲。不难发现,对于需要特殊处理的 \\(u\\),若 \\(fa_u\\) 有若干个需要特殊处理的儿子,其中只能有至多一个 \\(\\not \\in S\\)

我们先钦定所有需要特殊处理的点属于 \\(S\\),此时若 \\(fa_u\\) 所在连通块大小 \\(> 1\\),则其所有需要特殊处理的儿子均 \\(\\in S\\)。否则,\\(fa_u\\) 应有恰好一个儿子 \\(\\not \\in S\\)。即对于所有存在需要特殊处理的儿子,且当前所在连通块大小 \\(= 1\\)\\(v\\),给答案乘上 \\(v\\) 需要特殊处理的儿子数量

\\(x = 1\\) 时,我们应给答案乘上 \\(v\\) 需要特殊处理的儿子数量 加一。这是因为点 \\(v\\) 存在需要特殊处理的儿子,所以若 \\(v \\neq rt\\)\\(fa_v \\in S\\),因此点 \\(v\\) 需要特殊处理的儿子可以都 \\(\\in S\\)

同样这个过程可以用并查集加桶维护。

总时间复杂度 \\(O(n \\alpha(n))\\),瓶颈在并查集,但是精细实现可以不用并查集。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using ld = long double;
template <typename T>
void chkmax(T &x, const T &y) 
  if (x < y) x = y;

template <typename T>
void chkmin(T &x, const T &y) 
  if (y < x) x = y;

constexpr int MAXN = 1e5 + 10, MOD = 998244353, INF = 0x3f3f3f3f;
void inc(int &x, int y)  x += y, x >= MOD && (x -= MOD); 
void dec(int &x, int y)  x -= y, x < 0 && (x += MOD); 
int n, k, dfn[MAXN], low[MAXN], dfc, stk[MAXN], tp, num, sz[MAXN * 2];
int mini, rt, ans1[MAXN], ans2[MAXN], fa[MAXN * 2], rec[MAXN];
bool vis[MAXN * 2];
vector<int> og[MAXN], g[MAXN * 2], buc[MAXN];
namespace dsu 
int fa[MAXN], sz[MAXN], cnt[MAXN];
void init() 
  iota(fa + 1, fa + n + 1, 1);
  fill(sz + 1, sz + n + 1, 1);
  cnt[1] = n;

int find(int x)  return x == fa[x] ? x : fa[x] = find(fa[x]); 
void merge(int x, int y) 
  x = find(x);
  y = find(y);
  --cnt[sz[x]];
  --cnt[sz[y]];
  fa[x] = y;
  sz[y] += sz[x];
  ++cnt[sz[y]];

  // namespace dsu
void tarjan(int u) 
  dfn[u] = low[u] = ++dfc;
  stk[++tp] = u;
  for (auto v : og[u]) 
    if (!dfn[v]) 
      tarjan(v);
      chkmin(low[u], low[v]);
      if (low[v] == dfn[u]) 
        g[u].emplace_back(++num);
        g[num].emplace_back(u);
        while (true) 
          int t = stk[tp--];
          g[num].emplace_back(t);
          g[t].emplace_back(num);
          if (t == v) break;
        
      
     else 
      chkmin(low[u], dfn[v]);
    
  

void get_root(int u, int pre) 
  sz[u] = (u <= n);
  int tmp = 0;
  for (auto v : g[u]) 
    if (v == pre) continue;
    get_root(v, u);
    sz[u] += sz[v];
    chkmax(tmp, sz[v]);
  
  chkmax(tmp, n - sz[u]);
  if (tmp < mini) 
    mini = tmp;
    rt = u;
  

void dfs(int u, int pre) 
  fa[u] = pre;
  sz[u] = (u <= n);
  for (auto v : g[u]) 
    if (v == pre) continue;
    dfs(v, u);
    sz[u] += sz[v];
  
  if (u > n) buc[sz[u]].emplace_back(u);

int main() 
  freopen("cities.in", "r", stdin);
  freopen("cities.out", "w", stdout);
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  int m;
  cin >> n >> m >> k;
  while (m--) 
    int u, v;
    cin >> u >> v;
    og[u].emplace_back(v);
    og[v].emplace_back(u);
  
  num = n;
  tarjan(1);
  mini = INF;
  get_root(1, 0);
  if (rt > n) rt = g[rt][0];
  dfs(rt, 0);
  dsu::init();
  for (int i = 1; i < n; ++i) 
    for (auto u : buc[i]) 
      if (g[u].size() > 2) 
        for (int j = 1; j < (int)g[u].size(); ++j)
          dsu::merge(g[u][j - 1], g[u][j]);
      
    
    ans1[i] = (dsu::cnt[i] * i == n);
    vector<int> vec;
    for (auto u : buc[i]) 
      if (g[u].size() == 2) 
        if (dsu::sz[dsu::find(fa[u])] == 1) 
          vis[u] = vis[fa[u]] = true;
          dsu::merge(fa[u], g[u][g[u][0] == fa[u]]);
          vec.emplace_back(fa[u]);
          rec[fa[u]] = 1;
         else if (vis[fa[u]]) 
          ++rec[fa[u]];
        
      
    
    if (dsu::cnt[i] * i + dsu::cnt[i + 1] * (i + 1) == n) 
      ans2[i] = 1;
      for (auto u : vec) ans2[i] = (ll)ans2[i] * (rec[u] + (i == 1)) % MOD;
    
    for (auto u : buc[i]) 
      if (g[u].size() == 2 && !vis[u]) 
        for (int j = 1; j < (int)g[u].size(); ++j)
          dsu::merge(g[u][j - 1], g[u][j]);
      
    
  
  if (!k) 
    cout << accumulate(ans1 + 1, ans1 + n, 0) << "\\n";
   else 
    int ans = MOD - 1;
    for (int i = 1; i < n; ++i) inc(ans, ans2[i]);
    for (int i = 2; i < n; ++i) dec(ans, ans1[i]);
    cout << ans << "\\n";
  
  return 0;

P6627 [省选联考 2020 B 卷] 幸运数字

P6627 [省选联考 2020 B 卷] 幸运数字

这道题乍一看的确挺简单,但是我就是写挂了(统计答案的锅)。100->35,省队无。

看完后就会发现:你只需要维护一个区间异或的操作,并且不需要在线修改。

那么,差分不就完事了吗。

//在[l,r]上对a[i]进行区间异或
a[l] ^= w, a[r] ^= w;

//最后统计答案前
for(int i = 2; i <= n; i++) a[i] ^= a[i-1];

那么,只要离散化数据,很轻松地就可以完成这个操作。由于要离散化,时复O(nlogn)


那么,统计答案怎么办呢?

我们来找找答案在哪里把。

首先,我们肯定要把那些询问的边界(A, B, L, R)(姑且称为边界点)保存下来,这些东西当然可能成为答案。

其次,我们发现,两个相邻的边界点(比如叫做(A)(B))之间的一段区间([A+1,B-1])的答案必定是相同的。那么答案可能在这个区间上。那么问题来了:到底在哪里呢?根据这个鬼畜的答案统计规则,我们可以发现,它必定在区间的两端(即(A+1)(B-1))或(0)处。

所以这样统计就好了。

记得数组空间开大!!!

#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
ll read() {
   ll x = 0, f = 1; char ch = getchar();
   for(; ch < ‘0‘ || ch > ‘9‘; ch = getchar()) if(ch == ‘-‘) f = -1;
   for(; ch >= ‘0‘ && ch <= ‘9‘; ch = getchar()) x = x * 10 + ch - ‘0‘;
   return x * f;
}
const int MAXN = 1e5 + 5;
int n;
struct Node {
   int t, x, y, w;

}q[MAXN];
int lsh[MAXN  * 6], tot;
void LSH() {
   sort(lsh + 1, lsh + 1 + tot);
   tot = unique(lsh + 1, lsh + 1 + tot) - lsh - 1;
}
int LSH(int x) {
   return lower_bound(lsh + 1, lsh + 1 + tot, x) - lsh;
}
int a[MAXN * 6];
int maxans, pos;
int main() {
   n = read();
   for(int i = 1; i <= n; i++) {
      Node& now = q[i];
      now.t = read();
      if(now.t == 1)
         now.x = read(), now.y = read();
      else if(now.t == 2)
         now.x = read();
      else now.y = read();
      lsh[++tot] = now.x;
      lsh[++tot] = now.x - 1;
      lsh[++tot] = now.x + 1;
      lsh[++tot] = now.y;
      lsh[++tot] = now.y - 1;
      lsh[++tot] = now.y + 1;
      now.w = read();
   }
   lsh[++tot] = 0;
   LSH();
   for(int i = 1; i <= n; i++) {
      Node& now = q[i];
      int tx = LSH(now.x), ty = LSH(now.y);
      if(now.t == 1) a[tx] ^= now.w, a[ty + 1] ^= now.w;
      else if(now.t == 2) a[tx] ^= now.w, a[tx + 1] ^= now.w;
      else a[1] ^= now.w, a[ty] ^= now.w, a[ty + 1] ^= now.w;
   }
   for(int i = 2; i <= tot; i++) a[i] ^= a[i - 1];
   maxans = a[1]; pos = 1;
   for(int i = 2; i <= tot; i++) {
      if(maxans < a[i]) maxans = a[i], pos = i;
      else if(maxans == a[i]) {
         if(abs(lsh[i]) < abs(lsh[pos])) maxans = a[i], pos = i;
         else if(abs(lsh[i]) == abs(lsh[pos]) && lsh[i] > lsh[pos]) maxans = a[i], pos = i;
      }
   }
   printf("%d %d
", maxans, lsh[pos]);
   return 0;
}

以上是关于[P9167] [省选联考 2023] 城市建造的主要内容,如果未能解决你的问题,请参考以下文章

省选联考 2023 同步赛

树[省选联考2020]

P6627 [省选联考 2020 B 卷] 幸运数字

luoguP6623 [省选联考 2020 A 卷] 树(trie树)

P6625 [省选联考 2020 B 卷] 卡牌游戏

题解 省选联考2020 组合数问题