The 2024 ICPC Asia Chengdu Regional Contest(The 3rd Universal Cup. Stage 15: Chengdu)

写在前面

比赛地址:https://qoj.ac/contest/1821

以下按个人难度向排序。

混进赛站群偷取了补题链接周六打的。虽然看着歪掉的原榜但是题开得很顺但是写得很烂,反思!

同时因为在赛站群所以提前知道了有卡 0 逆元的题,场上稍微注意了一下呃呃有点不要脸了。

L 签到,构造

构造百分位数,一个显然的想法是构造一百个数。

于是根据题意构造即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int a,b,c;
int x[105];
signed main(){
	cin>>a>>b>>c;
	cout<<100<<endl;
	for(int i=1;i<=49;++i) x[i]=a; 
	x[50]=a;
	x[51]=a+1;
	for(int i=52;i<=94;++i) x[i]=a+1; 
	x[95]=b;
	x[96]=b+1;
	for(int i=97;i<=98;++i) x[i]=b+1; 
	x[99]=c;
	x[100]=c+1;
	for(int i=1;i<=100;++i){
		cout<<x[i]<<' ';
	}
	
	return 0;
}

J 签到,模拟

一开始大家都被骗去 G 了导致这水题没人开导致榜有点歪。

对于输入中的每一个 level 直接模拟即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m, q;
struct Data {
  int id;
  LL sum;
} a[kN];
int rk, nowlevel, nowtime, tag[kN], vis[kN];
//=============================================================
bool cmp(Data fir_, Data sec_) {
  if (fir_.sum != sec_.sum) return fir_.sum > sec_.sum;
  return fir_.id < sec_.id;
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> m >> q;
    nowtime = 0, rk = 0, nowlevel = 0;
    for (int i = 1; i <= m; ++ i) tag[i] = 0, vis[i] = 0, a[i].id = i, a[i].sum = 0; 

    for (int i = 1; i <= q; ++ i) {
      int opt; std::cin >> opt;
      if (opt == 1) {
        int x; std::cin >> x;
        if (x == nowlevel) continue;
        nowlevel = x, ++ nowtime, rk = 0;

      } else if (opt == 2) {
        int id, x; std::cin >> id >> x;
        if (x != nowlevel) continue;
        if (tag[id] != nowtime) tag[id] = nowtime, vis[id] = 0;
        if (!vis[id]) vis[id] = 1, a[id].sum += 1ll * m - rk, ++ rk;

      } else {
        int id, x; std::cin >> id >> x;
        if (x != nowlevel) continue;
        if (tag[id] != nowtime) tag[id] = nowtime, vis[id] = 0;
        vis[id] = 1;

      }
    }
    std::sort(a + 1, a + m + 1, cmp);
    for (int i = 1; i <= m; ++ i) {
      std::cout << a[i].id << " " << a[i].sum << "\n";
    }
  }
  return 0;
}

G 构造,结论,二进制

一开始大家都被骗去 G 了呃呃,虽然也确实是简单题。

wenqizhi 大爹大力手玩出来的。

仅需考虑相邻的两个数操作能产生多少新权值,发现对于两个权值 \(x, y\),能产生的新权值为:

\[\begin{cases} x\operatorname{and} y, x\operatorname{or} y, x\operatorname{xor} y\\ x \operatorname{or} y \operatorname{xor} x, x \operatorname{or} y \operatorname{xor} y\\ 0 \end{cases}\]

懒得证了,见官方题解。

#include<bits/stdc++.h>
using namespace std;

#define ll long long
#define ull unsigned long long

int read()
{
    int x = 0, f = 0; char c = getchar();
    while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
    while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    return f ? -x : x;
}

const int N = 1e5 + 5;
int a[N];
set<int> S;

int main() 
{
    int n = read();
    for(int i = 1; i <= n; ++i)
    {
        a[i] = read();
    }
    S.insert(0);
    for(int i = 2; i <= n; ++i)
    {
        int x = a[i - 1], y = a[i], z = x | y;
        S.insert(x), S.insert(y), S.insert(z);
        S.insert(x & y), S.insert(z ^ x), S.insert(z ^ y), S.insert(x ^ y);
    }
    printf("%d\n", S.size());
    return 0;
}

A 构造,括号匹配

dztlb 大神秒了。

发现构造出左侧的一个 > 均需要右侧的一个 >>> 对应,类似一个括号匹配的形式。手玩下发现有解当且仅当:

  • \(s_1\)>
  • \(s\) 的长度为 3 的后缀为 >>>
  • \(s\) 中至少有一个 -

在有解条件下,考虑预处理出 \(s\) 均为 > 的后缀长度。若恰好为 3,则一种显然的构造是枚举 \(s_1\sim s_{n - 4}\) 中所有 > 的位置 \(i\) 均进行操作 \((i, n - i + 1)\) 即可;若大于 3,仅需枚举后缀 \(j\) 不断进行操作 \((1, n - i - j)\) 构造出上述后缀再套用上述做法即可。

总时间复杂度 \(O(n)\) 级别。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int T,n;
const int N=1e5+5;
char s[N];
int tot;
int p[N],l[N];
signed main(){
	cin>>T;
	while(T--){
		tot=0;
		scanf("%s",s+1);
		n=strlen(s+1);
		int cnt=0;
		for(int i=n;i>=1;--i){
			if(s[i]=='>'){
				++cnt;
			}else{
				break;
			}
		}
		if(cnt<3){
			puts("No");
			continue;
		}
		if(s[1]!='>'){
			puts("No");
			continue;
		}
		if(n-cnt<2){
			puts("No");
			continue;
		}
		for(int i=n;i>(n-cnt+2);--i){
			if(s[i]=='>'){
				++tot;
				p[tot]=1,l[tot]=i-1+1;
			}else break;
		}
		int r=n-cnt+3;
		for(int i=1;i<r-2;++i){
			if(s[i]=='>'){
				++tot;
				p[tot]=i;
				l[tot]=r-i+1;
			}
		}
		cout<<"Yes "<<tot<<'\n';
		for(int i=1;i<=tot;++i){
			cout<<p[i]<<' '<<l[i]<<'\n';
		}
	}
	return 0;
}
/*
1
>-->-->>->>->>>>>>>
*/

I 思维,枚举,gcd

考虑对原数列差分,处理出原序列中所有极长的不降序列,记录他们的断点和长度 \(\operatorname{len}\)。设共有 \(c\) 段,则对于所有合法的 \(k\),一定不能有一段横跨断点,再考虑到最后一段可以不完整,则容易发现某个 \(k\) 合法,当且仅当它同时是 \(\operatorname{len}_1, \cdots, \operatorname{len}_{c - 1}\) 的一个因数,即答案为 \(d(\gcd(\operatorname{len}_1, \cdots, \operatorname{len}_{c - 1}))\)\(d(i)\) 代表 \(i\) 的因数个数)。

发现单点修改仅会影响差分后两个相邻位置的正负性,即影响至多 2 段相邻的极长的不降序列,于是小力讨论下很容易维护单次修改对极长的不降序列的长度构成的集合的影响。我们的做法是考虑 set 维护差分后负数的下标,单次修改仅需查询前驱与后继的负数位置即可。

长度的值域只有 \(2\times 10^5\),因数个数很容易埃氏筛预处理。现在主要问题为如何维护一个集合的全局 \(\gcd\),支持单次增加或减少某个数。

一个非常套路的做法是考虑线段树维护区间 \(\gcd\),若集合中某个数 \(i\) 出现次数不小于 1,则将位置 \(i\) 置为 \(i\),否则置 0,查询全局 \(\gcd\) 即可。单次修改时间复杂度 \(O(\log^2 n)\) 级别。

然而场上 wenqizhi 大爹想到了一个非常牛逼的转化。考虑预处理所有数的所有因数,记 \(\operatorname{cnt}_i\) 表示数 \(i\) 作为某个 \(\operatorname{len}\) 的因数的出现次数,记 \(\operatorname{sum}_k\) 表示 \(\operatorname{cnt}_i = k\) 的数的个数,则单次询问的答案即为 \(\operatorname{sum}_{c - 1}\)。单次修改直接枚举被添加或删除的数的所有因数,大力修改上述数组即可,时间复杂度上界为 \(O(\sqrt n)\) 级别但是完全跑不满喵。

总时间复杂度 \(O(n\ln n + q\sqrt n)\) 级别但是完全跑不满哈哈。

#include<bits/stdc++.h>
using namespace std;

#define ll long long
#define ull unsigned long long

int read()
{
    int x = 0, f = 0; char c = getchar();
    while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
    while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    return f ? -x : x;
}

const int N = 2e5 + 5;
int n, q;
int a[N], b[N];
set<int> S;
set<int>::iterator it1, it2;
int cnt[N]; // 约数i出现的次数
int sum[N]; // 出现次数为i的约数个数

vector<int> yve[N];

void del(int x)
{
    for(auto y : yve[x])
    {
        --sum[cnt[y]];
        --cnt[y];
        ++sum[cnt[y]];
    }
}

void add(int x)
{
    for(auto y : yve[x])
    {
        --sum[cnt[y]];
        ++cnt[y];
        ++sum[cnt[y]];
    }
}

void change(int pos, int op1, int op2)
{
    if(op1 == 1)
    {
        it1 = --(S.lower_bound(pos));
        it2 = S.lower_bound(pos);
        if(it2 == S.end())
        {
            add((pos) - (*it1));
            S.insert(pos);
        }else
        {
            del((*it2) - (*it1));
            add(pos - (*it1));
            add((*it2) - pos);
            S.insert(pos);
        }
    }else
    {
        S.erase(pos);
        it1 = --(S.lower_bound(pos));
        it2 = S.lower_bound(pos);
        // printf("%d %d\n", *it1, *it2);

        if(it2 == S.end())
        {
            del(pos - (*it1));
            // S.erase(pos);
        }else
        {
            del(pos - (*it1));
            del((*it2) - pos);
            add((*it2) - (*it1));
            // S.erase(pos);
        }
    }
}

void solve()
{
    a[0] = 0x7fffffff;
    n = read(), q = read();
    for(int i = 1; i <= n; ++i)
    {
        a[i] = read(), b[i] = (a[i] >= a[i - 1]);
    }
    sum[0] = n;
    for(int i = 1; i <= n; ++i) sum[i] = cnt[i] = 0;
    cnt[0] = 0;
    S.clear();
    for(int i = 1; i <= n; ++i)
    {
        if(b[i] == 0) S.insert(i);
    }

    it1 = S.begin(), it2 = ++S.begin();

    while(it2 != S.end())
    {
        add((*it2) - (*it1));
        ++it1, ++it2;
    }

    printf("%d\n", sum[(int)S.size() - 1]);

    while(q--)
    {
        int p = read(), v = read(), op = (v >= a[p - 1]);
        if(op != b[p]) change(p, b[p], op);
        if(p < n)
        {
            op = (a[p + 1] >= v);
            if(op != b[p + 1]) change(p + 1, b[p + 1], op);
            b[p + 1] = op;
        }
        a[p] = v, b[p] = (v >= a[p - 1]);

        printf("%d\n", sum[(int)S.size() - 1]);
    }
    
}

int main() 
{
    for(int i = 1; i <= 200000; ++i)
        for(int j = 1; j * i <= 200000; ++j)
            yve[i * j].emplace_back(i);
    int T = read();
    while(T--) solve();
    return 0;
}

B 枚举,DP

数据范围好小,感觉 \(O(n^3 + nq)\) 都能过。

需要计数,考虑 DP,记 \(f_{i, c_a, c_b, c_c}\) 表示填 ? 到前缀 \(1\sim i\),当前已经填了恰好 \(c_a\)a\(c_b\)b\(c_c\)c,且 \(1\sim i\) 满足相邻位置不相等的方案数。转移时讨论下当前位置是否为 ?,以及填什么字母即可。

考虑一次询问 \(x, y, z\),由上述 DP,答案即为:

\[\sum_{i\le x}\sum_{j\le y}\sum_{k\le z}f_{n, i, j, k}[i + j + k = \operatorname{cnt}] \]

直接做 DP 时空复杂度均为 \(O(n^4)\) 级别,单次询问时间复杂度 \(O(n^3)\) 级别,均无法承受,考虑优化。

\(1\sim i\) 中有 \(\operatorname{cnt}\)?,发现合法的状态恒有 \(c_a + c_b + \operatorname{c_c}= \operatorname{cnt}\),于是考虑删去状态的最后一维,实现时再滚动数组优化掉第一维。则上述 DP 空间复杂度变为 \(O(n^2)\) 级别,时间复杂度变为 \(O(n^3)\) 级别。

此时对于一次询问 \(x, y, z\),答案变为:

\[\sum_{i\le x}\sum_{j\le y}f_{n, i, j}[i + j\le \operatorname{cnt}] \]

观察发现上述范围是一个矩形的对角线右上角之和的形式,于是考虑前缀和优化矩形的每一行,单次询问时枚举一维 \(i\),再查询区间和即可。具体地,记:\(g_{i, j} = \sum_{k\le j} {f_{i, k}}\)\(g_{i, -1} = 0\),则对于一次询问,答案变为:

\[\sum_{0\le i\le \min({x, \operatorname{cnt}})} g_{i, \min(y, \operatorname{cnt}-i)} - g_{i, \max(-1, \operatorname{cnt} - i - z)} [i + y + z \ge \operatorname{cnt}] \]

总时间复杂度 \(O(n^3 + nq)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 310;
const LL p = 1e9 + 7;
//=============================================================
int n, q, cnt;
std::string s;
LL f[2][kN][kN][3], g[kN][kN];
//=============================================================
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n >> q;
  std::cin >> s; s = "$" + s;
  
  int now = 0;
  if (s[1] == '?') ++ cnt, f[1][1][0][0] = f[1][0][1][1] = f[1][0][0][2] = 1;
  else f[1][0][0][s[1] - 'a'] = 1;
  
  for (int i = 2; i <= n; ++ i, now ^= 1) {
    for (int ca = 0; ca <= i; ++ ca) {
      for (int cb = 0; cb <= i; ++ cb) {
        for (int type = 0; type <= 2; ++ type) {
          f[now][ca][cb][type] = 0;
        }
      }
    }

    if (s[i] != '?') {
      for (int ca = 0; ca <= cnt; ++ ca) {
        for (int cb = 0; cb <= cnt - ca; ++ cb) {
          for (int type = 0; type <= 2; ++ type) {
            if (type != s[i] - 'a') {
              (f[now][ca][cb][s[i] - 'a'] += f[now ^ 1][ca][cb][type]) %= p;
            }
          }
        }
      }
      continue;  
    }
    
    ++ cnt;
    for (int ca = 0; ca <= cnt; ++ ca) {
      for (int cb = 0; cb <= cnt - ca; ++ cb) {
        for (int type1 = 0; type1 <= 2; ++ type1) {
          for (int type2 = 0; type2 <= 2; ++ type2) {
            if (type1 == type2) continue;
            if (type2 == 0 && ca >= 1)  (f[now][ca][cb][0] += f[now ^ 1][ca - 1][cb][type1]) %= p;
            if (type2 == 1 && cb >= 1)  (f[now][ca][cb][1] += f[now ^ 1][ca][cb - 1][type1]) %= p;
            if (type2 == 2 && cnt - ca - cb >= 1)  (f[now][ca][cb][2] += f[now ^ 1][ca][cb][type1]) %= p;
          }
        }
      }
    }
  }


  for (int ca = 0; ca <= cnt; ++ ca) {
    for (int cb = 0; cb <= cnt - ca; ++ cb) {
      for (int type = 0; type <= 2; ++ type) {
        (g[ca][cb] += f[now ^ 1][ca][cb][type]) %= p;
      }
      if (cb > 0) (g[ca][cb] += g[ca][cb - 1]) %= p;
      // std::cout << g[ca][cb] << " ";
    }
    // std::cout << "\n";
  }

  while (q --) {
    int x, y, z; std::cin >> x >> y >> z;
    x = std::min(x, cnt);
    y = std::min(y, cnt);
    z = std::min(z, cnt);
    LL ans = 0;
    for (int ca = 0; ca <= x; ++ ca) {
      if (cnt - ca > y + z) continue;
      if (cnt - z - ca - 1 < 0) (ans += g[ca][std::min(cnt - ca, y)]) %= p;
      else (ans += g[ca][std::min(cnt - ca, y)] - g[ca][cnt - z - ca - 1] + p) %= p;
    }
    std::cout << ans << "\n";
  }
  return 0;
}
/*
20 1
????????????????????
20 20 20
*/

K 结论,费用流

注意:本做法在 CF 上被 11.3 新增的 hack 数据卡掉了导致无法通过,请参考官方题解并适当卡常。

我是天才,这题只过了两个队的时候就给开出来了,牛逼。

首先发现 \(n\) 太小了;又发现每个数被修改的变化过程构成一条权值递减的路径,其贡献即为路径长度,需要保证有且仅有 \(n\) 条路径产生贡献,于是考虑最大流;又发现某个权值可能被多条路径经过,即每条边可能贡献多次,于是考虑费用流。

将每种可能出现的权值 \(i\) 视为节点 \(i\),考虑如下的建图方案:

  • 对于所有原数列中的数 \(a_i\),源点向 \(a_i\) 连边,流量为 1,费用为 0;
  • 对于所有权值 \(i\),向其所有小于 \(i\) 的因数连边,流量为无限,费用为 1,代表一次操作;
  • 对于所有权值 \(i\),向汇点连边,流量为 1,费用为 0;

发现若上图可满流,说明与汇点连接的、对流量有贡献的 \(n\) 个点对应的互不相同的权值,构成了数列的合法的最终状态,则可知有 \(n\) 条上述路径产生了贡献,由最终状态权值互不相同合法可知,一定存在与上述路径对应的一种合法操作方案,使得在操作过程中所有权值也互不相同。则上图中的最大费用最大流的费用即为答案。

然而上图中第二类边数可能超级多,考虑优化。

一个显然的贪心结论是每次操作权值 \(a\) 时,仅会令其变为 \(\frac{a}{p}\),其中 \(p\)\(a\) 的某个质因数。正确性显然,这样做可以获得更多费用,且一定可以构造出合法的操作方案。

则此时点数上界为 \(O(n\sqrt n)\) 级别,每个点出边数量上界为 \(O(\log v)\) 级别,则点数边数均不大于 \(O(n^2)\) 级别,且最大流量只有 \(n\),稳过。

以下代码使用了 Primal-Dual 原始对偶算法。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pli std::pair<LL,int>
#define mp std::make_pair
const int kN = 5e6 + 10;
const int kM = 5e6 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int datanum, a[kN];

int n, S, T;
std::map<int, int> id;
bool done[kN];

int edgenum = 1, head[kN], v[kM], ne[kM];
bool vis[kN];
LL w[kM], c[kM], h[kN], dis[kN];
LL maxf, minc;
struct Previous_Node {
  int node, edge;
} from[kN];
//=============================================================
void Add(int u_, int v_, LL w_, LL c_) {
  v[++ edgenum] = v_; 
  w[edgenum] = w_;
  c[edgenum] = c_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;

  v[++ edgenum] = u_; 
  w[edgenum] = 0;
  c[edgenum] = -c_;
  ne[edgenum] = head[v_];
  head[v_] = edgenum;
}
void Spfa(int s_) {
  std::queue <int> q;
  for (int i = 0; i <= n; ++ i) {
    vis[i] = 0;
    h[i] = kInf;
  }
  q.push(s_);
  h[s_] = 0;
  vis[s_] = true;
 
  while (!q.empty()) {
    int u_ = q.front(); q.pop();
    vis[u_] = false;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      LL w_ = w[i], c_ = c[i];
      if (w_ && h[u_] + c_ < h[v_]) {
        h[v_] = h[u_] + c_;
        if (!vis[v_]) q.push(v_), vis[v_] = true;
      }
    }
  }
}
bool Dijkstra(int s_) {
  std::priority_queue<pli> q;
  for (int i = 1; i <= n; ++ i) {
    vis[i] = 0, dis[i] = kInf;
  }
  dis[s_] = 0;
  q.push(mp(0, s_));
  while (!q.empty()) {
    int u_ = q.top().second; q.pop();
    if (vis[u_]) continue;
    vis[u_] = true;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      LL w_ = w[i], nc_ = c[i] + h[u_] - h[v_];
      if (w_ && dis[u_] + nc_ < dis[v_]) {
        dis[v_] = dis[u_] + nc_;
        from[v_] = (Previous_Node) {u_, i};
        if (!vis[v_]) q.push(mp(-dis[v_], v_));
      }
    }
  }
  return dis[T] != kInf;
}
void MCMF() {
  Spfa(S);
  while (Dijkstra(S)) {
    LL minf = kInf;
    for (int i = 1; i <= n; ++ i) h[i] += dis[i];
    for (int i = T; i != S; i = from[i].node) minf = std::min(minf, w[from[i].edge]);
    for (int i = T; i != S; i = from[i].node) {
      w[from[i].edge] -= minf;
      w[from[i].edge ^ 1] += minf;
    }
    maxf += minf;
    minc += minf * h[T];
  }
  return ;
}
void dfs(int au_) {
  int u_ = id[au_];
  if (au_ == 1) return ;
  if (done[u_]) return ;
  done[u_] = 1;

  int temp = au_;
  for (int d = 2; d * d <= temp; ++ d) {
    if (au_ % d != 0) continue;
    int next = au_ / d;
    if (!id.count(next)) id[next] = ++ n;
    Add(u_, id[next], kInf, -1);
    dfs(next);

    while (temp % d == 0) temp /= d;
  }
  if (temp != 1) {
    int next = au_ / temp;
    if (!id.count(next)) id[next] = ++ n;
    Add(u_, id[next], kInf, -1);
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> datanum;
  S = ++ n, T = ++ n;

  for (int i = 1; i <= datanum; ++ i) {
    std::cin >> a[i];
    if (!id.count(a[i])) id[a[i]] = ++ n;
    Add(S, id[a[i]], 1, 0);
    dfs(a[i]);
  }
  for (auto [x, y]: id) Add(y, T, 1, 0);

  MCMF();
  std::cout << -minc << "\n";
  // std::cout << maxf << " " << -minc;
  return 0;
}

E 换根 DP,树上差分

首先我肯定做过单次询问为单点的版本,是典中典之换根 DP。这题只是又套了个树上差分了呃呃。

套路地求补集,对于一次询问答案为整棵树的连通子图数量,减去不包含路径 \(u\leftrightarrow v\) 中任意一个点的连通子图数量。记 \(\operatorname{lca}(u, v)\) 为节点 \(u, v\) 的最近公共祖先,画个图易知上述补集又可以拆成两部分:

  1. 子树 \(\operatorname{lca}(u, v)\) 之外的点的连通子图;
  2. 以路径 \(u\leftrightarrow v\) 上节点的、不在路径上的子节点为根的子树的连通子图。

对于第一部分是一个很典的换根 DP。记 \(f_{u}\) 表示钦定包含节点 \(u\) 的、子树 \(u\) 的连通子图数量;\(g_{u}\) 表示不一定包含 \(u\) 的、子树 \(u\) 的连通子图数量。初始化叶节点 \(f_u = g_u = 1\),则有显然的转移:

\[\begin{aligned} &f_u = \prod_{v\in \operatorname{son}(u)} (f_{v} + 1)\\ &g_{u} = f_u + \sum_{v \in \operatorname{son}(u)} g_v \end{aligned}\]

换根 DP 时考虑记 \(f'_u\) 表示子树 \(u\) 以外部分,钦定包含节点 \(u\) 的连通子图数量;\(g'_{u}\) 表示不一定包含 \(u\) 的连通子图数量。为了方便转移,额外记 \(h_u = \sum_{v\in \operatorname{son}(u)} g_v\),则有:

\[\begin{aligned} &f'_u = \frac{f'_{\operatorname{fa}_u} \times f_{\operatorname{fa}_u}}{f_{u} + 1} = (f'_{\operatorname{fa}_u} + 1)\times \prod_{v\in \operatorname{son}(\operatorname{fa}_u), v\not= u} (f_{v} + 1)\\ &g'_u = g'_{\operatorname{fa}_u} + f'_u + \sum_{v\in \operatorname{son}(\operatorname{fa}_u), v\not= u } g_{v} = g'_{\operatorname{fa}_u} + f'_u +h_{\operatorname{fa}_u} - h_u \end{aligned}\]

注意为了避免乘 0 的逆元出现,需要将 \(f'_u\) 的转移方程改为上式的右半部分。

则对于一次询问 \(u, v\),第一部分的贡献即为 \(g'_{\operatorname{lca}(u, v)}\),预处理后即可 \(O(1)\) 查询。

然后考虑第二部分的询问,由上述状态可以写出式子:

\[\sum_{i\in u\leftrightarrow v} \sum_{j\in \operatorname{son}(i), j\not \in u\leftrightarrow v} g_{j} = g_{\operatorname{lca}(u, v)} - \sum_{i\in u\leftrightarrow v} f_i \]

直接算复杂度是 \(O(n)\) 的,显然不行,由多组询问可知我们至多允许 \(O(\log n)\) 左右的单次询问。因为查询的是某条路径上的信息之和的形式,套路地使用树上差分维护并快速查询 \(\sum f\) 即可。

则对于一次询问 \(u, v\),答案为:

\[g_1 - \left(g'_{\operatorname{lca}(u, v)} + g_{\operatorname{lca}(u, v)} - \sum_{i\in u\leftrightarrow v} f_i\right) \]

\(O(n)\) 预处理后,单次查询仅需求 \(\operatorname{lca}\) 即可,总时间复杂度 \(O(n + q\log n)\) 级别。

注意实现时需要避免乘 0 的逆元出现,在换根转移 \(f'_u\) 时,考虑先对所有子节点排序,然后对他们的 \(f_v + 1\) 求前缀积、后缀积,转移时使用前后缀积进行转移从而避免除法出现。

wenqizhi 大爹大力码一发直接 1A 了太吊了。

#include<bits/stdc++.h>
using namespace std;

#define ll long long
#define ull unsigned long long

int read()
{
    int x = 0, f = 0; char c = getchar();
    while(c < '0' || c > '9') f |= (c == '-'), c = getchar();
    while(c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    return f ? -x : x;
}

const int N = 1e5 + 5;
const ll mod = 998244353;

ll qpow(ll a, ll b, ll mod)
{
    ll ans = 1;
    while(b)
    {
        if(b & 1) ans = ans * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return ans;
}
int n, q;
vector<int> to[N];

int FA[20][N], depth[N];
ll f[N], sumf[N], g[N], h[N], ff[N], gg[N];
vector<ll> pref[N], suff[N];

void dfs1(int k, int fa)
{
    f[k] = 1;
    FA[0][k] = fa, depth[k] = depth[fa] + 1;
    for(int i = 1; i <= 19; ++i) FA[i][k] = FA[i - 1][FA[i - 1][k]];
    for(auto v : to[k])
    {
        dfs1(v, k);
        f[k] = f[k] * (f[v] + 1) % mod;
        g[k] = (g[k] + g[v]) % mod;
        h[k] = (h[k] + g[v]) % mod;
    }
    g[k] = (g[k] + f[k]) % mod;
}

int LCA(int x, int y)
{
    if(depth[x] < depth[y]) swap(x, y);
    for(int i = 19; i >= 0; --i)
        if(depth[FA[i][x]] >= depth[y]) x = FA[i][x];
    if(x == y) return x;
    for(int i = 19; i >= 0; --i)
        if(FA[i][x] != FA[i][y]) x = FA[i][x], y = FA[i][y];
    return FA[0][x];
}

void dfs3(int k, int fa)
{
    sumf[k] = (sumf[fa] + f[k]) % mod;
    for(int i = 0; i <= (int)to[k].size() + 1; ++i)
    {
        pref[k].emplace_back(1);
        suff[k].emplace_back(1);
    }
    if(to[k].size())
    {
        pref[k][1] = f[to[k][0]] + 1;
        suff[k][to[k].size()] = f[to[k][to[k].size() - 1]] + 1;
        for(int i = 1; i < (int)to[k].size(); ++i)
        {
            pref[k][i + 1] = pref[k][i] * (f[to[k][i]] + 1) % mod;
        }
        for(int i = (int)to[k].size() - 1; i >= 1; --i)
        {
            suff[k][i] = suff[k][i + 1] * (f[to[k][i - 1]] + 1) % mod;
        }
    }
    for(auto v : to[k]) dfs3(v, k);
}

void dfs2(int k, int fa)
{
    for(int i = 0; i < (int)to[k].size(); ++i)
    {
        ff[to[k][i]] = (ff[k] + 1) * pref[k][i] % mod * suff[k][i + 2] % mod;
        gg[to[k][i]] = (ff[to[k][i]] + h[k] - g[to[k][i]] + gg[k] + mod) % mod;
        dfs2(to[k][i], k);
    }
}

void print(int k, int fa)
{
    printf("k = %d, f = %lld, g = %lld, ff = %lld, gg = %lld\n", k, f[k], g[k], ff[k], gg[k]);
    for(auto v : to[k]) print(v, k);
}

void solve()
{
    n = read(), q = read();
    for(int i = 1; i <= n; ++i)
    {
        to[i].clear();
        pref[i].clear();
        suff[i].clear();
        depth[i] = 0;
        f[i] = g[i] = h[i] = ff[i] = gg[i] = sumf[i] = 0;
    }

    for(int i = 2; i <= n; ++i)
    {
        int p = read();
        to[p].emplace_back(i);
    }

    dfs1(1, 0);
    dfs3(1, 0);
    dfs2(1, 0);
    // print(1, 0);

    while(q--)
    {
        int u = read(), v = read(), lca = LCA(u, v);
        ll ans = (sumf[u] + sumf[v] - sumf[lca] - sumf[FA[0][lca]] + mod + mod) % mod;
        ans = (-ans + g[lca] + gg[lca] + mod) % mod;
        printf("%lld\n", (g[1] - ans + mod) % mod);
    }
}

int main() 
{
    int T = read();
    while(T--) solve();
    return 0;
}

D 枚举,构造

其实场上半个小时就开出来这题了,但是发现没人过就不敢继续看了呃呃。

排列 \(q\) 是排列 \(p\) 的一个错排,显然有 \(|p_i - q_i|\ge 1\),则有下界 \(\sum_i |p_i - q_i| \ge n\)

手玩下容易发现当 \(n\) 为偶数时,考虑对于 \(\forall 1\le i\le n, i\text { odd}\),交换排列中权值 \(i, i + 1\) 的位置得到 \(q\) 即可取到下界,且显然构造是唯一的,则当且仅当 \(k=1\) 时有解。

\(n\) 为奇数时,由于无法相邻权值交换,下界变为 \(n + 1\);再手玩下发现也可以下界,当且仅当选择权值区间 \([i, i + 2](i \text{ odd})\),独立地构造该权值区间(且只有两种构造方式),对于其他权值套用 \(n\) 为偶数的构造即可。则构造方式共有 \(\frac{n-1}{2}\times 2 = n-1\) 种。

发现构造出来的 \(n\) 个排列,通过独立处理的 3 个位置和形态就能唯一地描述。要比较两个排列的字典序,仅需找到他们的最长公共前缀的长度,然后比较第一个不相同的位置即可。所以理论上可以实现一个 \(O(1)\)\(O(\log n)\) 的比较函数,直接排序或使用 nth_element 即可求得第 \(k\) 小。

实际上也确实可以实现。考虑两个排列 \(q_1, q_2\) 独立构造的权值区间分别为 \([x_1, x_1 + 2], [x_2, x_2 + 2]\),若 \(x_1=x_2\) 则其他部分完全相同,直接比较上述权值区间中对应第一个位置即可。

对于 \(x_1\not= x_2\),手玩下容易发现对于权值区间 \([1, \min(x_1, x_2)]\)\([\max(x_1 + 2, x_2 + 2), n]\) 的构造是完全相同的,则上述权值区间之外的、构造不同的权值区间为 \([\min(x_1, x_2) + 3, \max(x_1, x_2) - 1]\)(如果存在的话)。于是考虑比较 \([x_1, x_1 + 2], [x_2, x_2 + 2]\) 对应的六个位置,再加上在上述区间中最靠前的位置共七个位置进行检查,即可保证取到 \(q_1, q_2\) 第一个不同的位置。

取区间最小值考虑 ST 表,则单次比较排列字典序时间复杂度为 \(O(\log n)\) 级别。取第 \(k\) 小使用 nth_element,总时间复杂度 \(O(n\log n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 2e5 + 10;
//=============================================================
int n, k, ans, p[kN], pos[kN], q[kN];

namespace ST {
  int mylg2[kN], f[kN][21];
  void init() {
    mylg2[1] = 0;
    for (int i = 2; i <= n; ++ i) mylg2[i] = mylg2[i >> 1] + 1;
    for (int i = 1; i <= n; ++ i) f[i][0] = pos[i];
    for (int i = 1, l = 2; i <= 20; ++ i, l <<= 1) {
      for (int j = 1; j + l - 1 <= n; ++ j) {
        f[j][i] = std::min(f[j][i - 1], f[j + l / 2][i - 1]);
      }
    }
  }
  int query(int l_, int r_) {
    if (l_ > r_) return -1;

    int l = mylg2[r_ - l_ + 1];
    return std::min(f[l_][l], f[r_ - (1 << l) + 1][l]);
  }
}

struct Data {
  int l;
  pr<int, int> a[4];
  int get(int x_) {
    for (int i = 0; i < 3; ++ i) if (x_ == a[i].first) return a[i].second;
    int v = p[x_];
    if (v < l) return v % 2 ? v + 1 : v - 1;
    return v % 2 ? v - 1 : v + 1;
  }

  bool operator < (Data &sec_) {
    if (l == sec_.l) return a[0].second < sec_.a[0].second;

    std::set<int> s;
    for (int i = 0; i < 3; ++ i) s.insert(a[i].first), s.insert(sec_.a[i].first);
    s.insert(ST::query(std::min(l, sec_.l) + 3, std::max(l, sec_.l) - 1));

    for (auto x: s) {
      if (x < 1) continue;
      int v1 = get(x), v2 = sec_.get(x);
      if (v1 == v2) continue;
      return v1 < v2;
    }
    return 1;
  }
};
//=============================================================
void solve0() {
  if (k != 1) {
    ans = -1;
    return ;
  }
  ans = 1;
  for (int i = 1; i <= n; i += 2) std::swap(p[pos[i]], p[pos[i + 1]]);
}
void solve1() {
  ST::init();
  std::vector<Data> a;
  for (int i = 1; i + 2 <= n; i += 2) {
    Data now;
    now.l = i;
    now.a[0] = mp(pos[i], i + 1);
    now.a[1] = mp(pos[i + 1], i + 2);
    now.a[2] = mp(pos[i + 2], i);
    std::sort(now.a, now.a + 3);
    a.push_back(now);

    now.a[0] = mp(pos[i], i + 2);
    now.a[1] = mp(pos[i + 1], i);
    now.a[2] = mp(pos[i + 2], i + 1);
    std::sort(now.a, now.a + 3);
    a.push_back(now);
  }

  if (k > (int) a.size()) {
    ans = -1;
    return ;
  }
  std::nth_element(a.begin(), a.begin() + k - 1,  a.end());
  Data now = a[k - 1];
  for (int i = 0; i < 3; ++ i) p[now.a[i].first] = now.a[i].second;
  for (int i = 1; i + 1 < now.l; i += 2) std::swap(p[pos[i]], p[pos[i + 1]]);
  for (int i = now.l + 3; i + 1 <= n; i += 2) std::swap(p[pos[i]], p[pos[i + 1]]);
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    std::cin >> n >> k;
    for (int i = 1; i <= n; ++ i) std::cin >> p[i], q[i] = p[i], pos[p[i]] = i;
    ans = 1;
    if (n % 2 == 0) solve0();
    else solve1();

    if (ans == 1) {
      for (int i = 1; i <= n; ++ i) std::cout << p[i] << " ";
      std::cout << "\n";
    } else {
      std::cout << -1 << "\n";
    }
  }
  return 0;
}

写在最后

唉唉罚的太烂了。

学到了什么:

  • E:0 逆元的处理,考虑优化转移式,避免使用除法进行转移。
  • D:大胆开题。
posted @ 2024-11-04 14:00  Luckyblock  阅读(764)  评论(3编辑  收藏  举报