The 2023 ICPC Asia Jinan Regional Contest

写在前面

比赛地址:https://codeforces.com/gym/104901

以下按个人向难度排序。

SUA 的题确实牛逼,充分暴露出我这种傻逼学艺不精的本质,把我这种只会套路的沙比狠狠腐乳了。

D

签到。

直接枚举 \([L, \min(R, L + 10)]\) 检查即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
LL check(LL x_) {
  LL ret = 0;
  while (x_) {
    ret = std::max(ret, x_ % 10);
    x_ /= 10;
  }
  return ret;
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    LL la, ra, lb, rb; std::cin >> la >> ra >> lb >> rb;
    LL L = la + lb, R = ra + rb, ans = 0;
    for (LL i = L; i <= std::min(L + 10, R); ++ i) {
      ans = std::max (check(i), ans);
    }
    std::cout << ans << "\n";
  }
  return 0;
}

I

构造。

一个显然的想法是每次找到最小的无序的数值,设其位置为 \(p\),将其调整到第一个无序的位置 \(q\)。显然有 \(q<p,q < a_q\)\((q, p)\) 一定构成逆序对。

然而直接这样做会被形如 5 1 2 3 4 这样的数据卡掉,原因是仅能保证每次只有最小的无序的数值被调整到有序位置,操作次数上限为 \(O(n)\) 级别。

于是考虑优化一下上述过程,考虑能否至少一次排两个位置,发现仅需将寻找最小的无序的数值改成寻找 \(a_{q} - 1\) 即可保证每次使前缀 \([1, a_{q}]\) 变为有序的,至少新增 \([q, a_{q}]\) 中至少两个有序位置,操作次数上限变为 \(O(\left\lfloor\frac{n}{2}\right\rfloor)\) 级别。

数据范围小的一批随便实现。

Code by hjxddl

//Coded by hjxddl
#include<cstdio>
#include<iostream>
#include<algorithm> 
#include<iomanip>
#include<cstring>
#include<map>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N=2e5+5;
int t,n,a[105],ans1[105],ans2[105];
int x,x1,y,y1;
bool check(){
	for(int i=1;i<=n;i++){
		if(a[i]!=i){
			x=a[i],x1=i;
			return 1;
		}
	}
	return 0;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>t;
	while(t--){
		cin>>n;
		for(int i=1;i<=n;i++)
			cin>>a[i];
		int ans=0;
		while(check()){
			for(int i=x1;i<=n;i++)
				if(x>a[i])
					y1=i;
			ans1[++ans]=x1;
			ans2[ans]=y1;
			sort(a+x1,a+y1+1);
		}
		cout<<ans<<'\n';
		for(int i=1;i<=ans;i++)
			cout<<ans1[i]<<" "<<ans2[i]<<"\n";
	}
}

A

思维。

以下是赛时的杀软做法。

发现一个括号串存在二义性等价于存在四个位置 \(i, j, k, l\)

  • \(i, j, k, l\) 为同种括号。
  • \([i + 1, j - 1], [j + 1, k - 1], [k + 1, l - 1]\) 均可转化为合法括号串。

则可以将这四个位置进行如下转换:

\[(\cdots(\cdots)\cdots) \iff (\cdots)\cdots(\cdots) \]

保证给定的括号串一定存在一种合法构造,于是先考虑用栈按照最左优先匹配搞出一种来,则若存在满足上述条件的四个位置 \(i, j, k, l\),它们对应的区间一定被构造成了 \((\cdots)\cdots(\cdots)\) 的形式。

若位置 \(i\) 在构造方案中为右括号,则记位置 \(i\) 匹配的括号位置为 \(p_i\),考虑枚举每一对匹配的括号 \((p_i, i)\)

  • 检查 \((p_{p_i - 1}, i - 1)\) 是否存在,若存在检查其类型。若为同种则可构成 \((\cdots)(\cdots)\) 的形式说明有二义性。
  • 若不同种,检查 \((p_{p_{p_i - 1}}, p_{i - 1} - 1)\) 是否存在,若存在检查其类型。若与 \((p_i, i)\) 同种则可构成 \((\cdots)[\cdots](\cdots)\) 的形式说明有二义性;若不同种则可构成 \([\cdots][\cdots](\cdots)\) 的形式,这种情况同样有二义性,且一定会在枚举到 \((p_{p_{i} - 1}, p_{i} - 1)\) 的时候被检查到。

按照上述过程讨论即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 1e6 + 10;
//=============================================================
std::map <char, int> type;
//=============================================================
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  type['('] = type[')'] = 0;
  type['['] = type[']'] = 1;


  int T; std::cin >> T;
  while (T --) {
    std::string s;
    int flag = 0;
    std::cin >> s;
    std::stack<int> st1, st2;
    std::map <int,int> m;
    m.clear();

    for (int i = 0, len = s.length(); i < len; ++ i) {
      int key = type[s[i]];
      if (st1.empty()) {
        st1.push(i), st2.push(key);
      } else if (key != st2.top()) {
        st1.push(i), st2.push(key);
      } else {
        m[i] = st1.top();
        m[m[i]] = i;
        st1.pop(), st2.pop();
      }
    }

    for (int i = 0, len = s.length(); i < len; ++ i) {
      int p = m[i];
      if (p > i) continue;

      if (p - 1 < 0) continue;
      int q = m[p - 1];
      if (q > p - 1) continue;
      if (type[s[p - 1]] == type[s[i]]) {
        flag = 1;
        break;
      }

      if (q - 1 < 0) continue;
      int l = m[q - 1];
      if (l > q - 1) continue;
      if (type[s[q - 1]] == type[s[i]]) {
        flag = 1;
        break;
      }
    }
    std::cout << (flag ? "No" : "Yes") << "\n";
  }
  return 0;
}

赛后看了题解妈的场上太暴力了。

手玩发现有唯一构造的括号串实际上均形如 ([([...])]) 这样两种括号交替嵌套,或者为两个外层括号不同的上述括号串 ([(...)])[([...])]

  • 若有两个外层括号相同的上述括号串,如 ([...])([...]) 显然有二义性。
  • 若有两个以上的上述括号串,如 ([...])[(...)]([...]) 显然可以构成上述 (...)...(...) 的二义性形式。

发现只要有相邻的两个位置 \(s_i, s_{i + 1}\) 为同种括号,说明出现了一个两种括号交替嵌套的子串(此类子串的最内层括号),或者有两个上述子串最外层括号相同。

于是仅需检查相邻的两个位置 \(s_i, s_{i + 1}\) 为同种括号的情况是否出现不大于 2 次即可。

G

图论转化,种类并查集

典!然而学艺不精场上恼弹只建了单倍节点的图导致传递性根本不成立了一直 WA 呃呃太飞舞

首先特判 \(c\) 为奇数则最中间一列上有超过 1 个 1 则必然无解;然后发现对于第 \(j\) 列与第 \(n - j + 1\)\((1\le j\le \frac{c}{2})\) 中,若出现超过 2 个 1 则必然无解。又发现若第 \(j\) 列与第 \(n - j + 1\)\((1\le j\le \frac{c}{2})\) 中仅有 1 个 1,则此列并不会对翻转有任何限制;若有 2 个 1,设对应的两行分别为 \(p, q\),则这两行的翻转是互相限制的。具体地:

  1. 若这两行这两列的 1 在同一列,则它们之中必须翻转且仅翻转一个
  2. 若不在同一列,则它们必须同时翻转或同时不翻转

这东西一眼种类并查集典题,考虑建立图论模型检查连通性。具体地:

  • 建立 \(2\times n\) 个节点,其中第 \(i, n + i(1\le i\le n)\) 个节点分别代表第 \(i\) 行的操作与其对立操作。
  • 对于上述第 1 种限制,连边 \((p, q + n), (q + n, q)\) 表示若对互相限制的两行中某行进行操作,则必然需要对另一行进行对立的操作。
  • 对于第二种限制,连边 \((p, q)\)\((p + n, q + n)\) 表示若对互相限制的两行中某行进行操作,则必然需要对另一行进行相同的操作。

连边后对于所有 \(i(1\le i\le n)\) 检查 \(i\)\(i + n\) 是否连通,若连通说明不存在合法的操作方案无解,否则其中所有连通块都代表其中的所有行的两种操作方案,且不同连通块之间的操作没有限制互不影响。但考虑到图有双倍节点且每个连通块都有对称连通块,记图中连通块数量为 \(c\),则最终答案为 \(2^{\frac{c}{2}}\)

并查集维护即可,总时间复杂度为 \(O\left(\sum n \alpha(n)\right)\) 级别。

妈的看到题解一瞬间就想到之前做过一模一样套路的题了妈的:P4434 [COCI2017-2018#2] ​​Usmjeri

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 1e6 + 10;
const LL mod = 1e9 + 7;
//=============================================================
int n, m, flag, fa[kN << 1], vis[kN << 1];
std::vector<pii> row[kN];
LL ans;
//=============================================================
int find(int x_) {
  return (x_ == fa[x_]) ? x_ : (fa[x_] = find(fa[x_]));
}
void merge(int x_, int y_) {
  int fax = find(x_), fay = find(y_);
  fa[fax] = fay;
}
void Init() {
  std::cin >> n >> m;
  flag = 0;
  for (int i = 1; i <= n; ++ i) {
    fa[i] = i, fa[n + i] = n + i;
    vis[i] = vis[n + i] = 0;
  }
  for (int i = 1; i <= m; ++ i) row[i].clear();

  for (int i = 1; i <= n; ++ i) {
    std::string s; std::cin >> s;
    for (int j = 1; j <= m; ++ j) {
      if (flag) break;
      if (s[j - 1] == '0') continue;

      int p = std::min(j, m - j + 1);
      if (row[p].size() >= 2) flag = 1;
      if (row[p].size() < 2) row[p].push_back(mp(i, j));
    }
  }
  if (m % 2 == 1 && row[m / 2 + 1].size() > 1) flag = 1;
}
void Solve() {
  for (int i = 1; i <= m / 2; ++ i) {
    if (row[i].size() == 2 && row[i][0].first != row[i][1].first) {
      if (row[i][0].second == row[i][1].second) {
        merge(row[i][0].first, row[i][1].first + n);
        merge(row[i][0].first + n, row[i][1].first);
      } else {
        merge(row[i][0].first, row[i][1].first);
        merge(row[i][0].first + n, row[i][1].first + n);
      }
    }
  }

  int cnt = 0;
  for (int i = 1; i <= 2 * n; ++ i) {
    if (i <= n && find(i) == find(n + i)) {
      flag = 1;
      return ;
    }
    if (!vis[find(i)]) {
      vis[find(i)] = 1;
      ++ cnt;
    }
  }

  cnt /= 2, ans = 1;
  while (cnt) -- cnt, ans = 2ll * ans % mod;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int T; std::cin >> T;
  while (T --) {
    Init();
    if (!flag) Solve();
    if (flag) ans = 0;
    std::cout << ans << "\n";
  }
  return 0;
}

K

枚举,数据结构

唉学艺不精当时觉得没用就没学对顶堆不会动态维护中位数太菜了,以及赛时光他妈想着二分答案了把尺取法忘了也是唐中唐

首先令 \(a_i:= a_i - i\),则令区间变为公差为 1 的等差数列转化为令区间相等。显然对于某个区间,将其全部修改为相等的最小代价,即为将它们全部改为中位数的代价之和。于是考虑如何动态维护中位数,以及维护大于/小于中位数的数的数量与和。

发现答案显然存在单调性,即若区间 \([l, r]\) 可在代价 \(k\) 内修改为全部相等则其任意子区间均也可以。于是考虑尺取法对于每个左端点求其最远的合法区间的右端点,在此过程中需要支持动态插入/删除/查询集合中位数/查询集合元素数量/查询集合元素之和,使用对顶 mulitiset 维护即可,维护过程详见:「笔记」对顶堆动态维护中位数 - Luckyblock

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

注意 multiset.erase 时,若传入的为值则会将所有值均删除;传入迭代器才仅会删除一个。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, ans;
LL k, a[kN], sum[2];
std::multiset<LL> less, greater;
//=============================================================
void init() {
  sum[0] = sum[1] = 0;
  less.clear(), greater.clear();
  less.insert(-kInf), greater.insert(kInf);
}
void adjust() {
  while (less.size() > greater.size() + 1) {
    std::multiset<LL>::iterator it = (--less.end());
    sum[0] -= *it, sum[1] += *it;
    greater.insert(*it);
    less.erase(it);
  }
  while (greater.size() > less.size()) {
    std::multiset<LL>::iterator it = greater.begin();
    sum[0] += *it, sum[1] -= *it;
    less.insert(*it);
    greater.erase(it);
  }
}
void add(int pos_) {
  if (a[pos_] <= *greater.begin()) less.insert(a[pos_]), sum[0] += a[pos_];
  else greater.insert(a[pos_]), sum[1] += a[pos_];
  adjust();
}
void del(int pos_) {
  std::multiset<LL>::iterator it = less.lower_bound(a[pos_]);
  if (it != less.end()) {
    sum[0] -= a[pos_];
    less.erase(it);
  } else {
    it = greater.lower_bound(a[pos_]);
    sum[1] -= a[pos_];
    greater.erase(it);
  }
  adjust();
}
int get_middle() {
  return *less.rbegin();
}
LL get_need() {
  LL szl = less.size() - 1, szr = greater.size() - 1, m = get_middle();
  return (szl * m - sum[0]) + (sum[1] - szr * m); 
}
bool check(int pos_) {
  add(pos_);
  if (get_need() <= k) return true;
  del(pos_);
  return false;
}
//=============================================================
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 >> a[i];
      a[i] -= i;
    }

    ans = 1;
    init();
    for (int l = 1, r = 0; l <= n; ++ l) {
      while (r + 1 <= n && check(r + 1)) ++ r;
      ans = std::max(ans, r - l + 1);
      del(l);
    }
    std::cout << ans << "\n";
  }
  return 0;
}

M

计算几何。

妈的计算几何入侵铜牌银牌题是吧,SUA 你们干的这是什么好事了!

唉数论还能做一做,一点多项式和计算几何都不会感觉不行了,感觉需要去补一下技能树呃呃。

E

网络流。

第一次见跑完网络流在残量网络上处理的题,唉学艺不精网络流只会打板子还是飞舞

首先建图最大流跑出一组原图的最大匹配的残量网络。若添加边 \((u, v)\) 后使最大匹配数加 1,说明加入 \((u, v)\) 后可以构成一条新的增广路,即此时的残量网络上存在两条路径 \(S\rightarrow u, v\rightarrow T\)

  • \(u\) 为二分图的左侧点,\(v\) 为二分图的右侧点。
  • \(S\rightarrow u\),$v\rightarrow $ 上所有边容量均为 1。

考虑分别求得满足上述条件的左右侧点集 \(s_1, s_2\),显然任意从 \(s_1, s_2\) 中选择一对点构成边均可得到一条新的增广路,则答案即为 \(|s_1|\times |s_2|\)。根据求增广路的过程,考虑从 \(S\) 开始进行 BFS,钦定仅可经过容量为 1 的边,可到达的所有左侧点即为点集 \(s_1\);同理从 \(T\) 开始 BFS 钦定仅可经过容量为 0 的边可到达的所有右侧点即为 \(s_2\)

总时间复杂度上界 \(O(\sum m\sqrt n + (n + m))\) 级别,这题时限卡得比较紧需要注意实现,除输出答案部分均应使用 int 型。

下面的代码跑了 2.8s 呃呃勉强能过。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
const int kM = 1e6 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n, m, S, T;
int nodenum, edgenum = 1, v[kM], ne[kM], head[kN], w[kM];
int cur[kN], dep[kN];
bool vis[kN];
//=============================================================
void Add(int u_, int v_, int w_) {
  v[++ edgenum] = v_;
  w[edgenum] = w_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void Init() {
  std::cin >> n >> m;
  S = 0, T = 2 * n + 1;
  nodenum = T, edgenum = 1;
  for (int i = 0; i <= nodenum; ++ i) head[i] = 0;

  for (int i = 1; i <= n; ++ i) Add(S, i, 1), Add(i, S, 0);
  for (int i = n + 1; i <= 2 * n; ++ i) Add(i, T, 1), Add(T, i, 0);
  for (int i = 1; i <= m; ++ i) {
    int u_, v_; std::cin >> u_ >> v_;
    Add(u_, v_ + n, 1), Add(v_ + n, u_, 0);
  }
}
 
bool BFS() {
  std::queue <int> q;
  memset(dep, 0, (nodenum + 1) * sizeof (int));
  dep[S] = 1; //注意初始化 
  q.push(S); 
  while (!q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      int w_ = w[i];
      if (w_ > 0 && !dep[v_]) {
        dep[v_] = dep[u_] + 1;
        q.push(v_);
      }
    }
  }
  return dep[T];
}
int DFS1(int u_, int into_) {
  if (u_ == T) return into_; 
  int ret = 0;
	for (int i = cur[u_]; i && into_; i = ne[i]) {
    int v_ = v[i];
    int w_ = w[i];
    if (w_ && dep[v_] == dep[u_] + 1) {
			int dist = DFS1(v_, std::min(into_, w_));
			if (!dist) dep[v_] = kN;
			into_ -= dist; 
      ret += dist;
      w[i] -= dist, w[i ^ 1] += dist;
			if (!into_) return ret;
		}
  }
	if (!ret) dep[u_] = 0; 
  return ret;
}
int Dinic() {
  int ret = 0;
  while (BFS()) {
    memcpy(cur, head, (nodenum + 1) * sizeof (int));
    ret += DFS1(S, kInf);
  }
  return ret;
}
int check(int s_, int l_, int r_, int w_) {
  int ret = 0;
  std::queue<int> q;
  memset(vis, 0, (nodenum + 1) * sizeof (bool));
  q.push(s_); vis[s_] = 1;
  while (!q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      if (vis[v_] || w[i] != w_) continue;
      q.push(v_), vis[v_] = 1;
      if (l_ <= v_ && v_ <= r_) ++ ret;
    }
  }
  return ret;
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int datanum; std::cin >> datanum;
  while (datanum --) {
    Init();
    Dinic();
    // for (int i = S; i <= T; ++ i) {
    //   for (int j = head[i]; j; j = ne[j]) {
    //     std::cout << i << " " << v[j] << " " << w[j] << "\n";
    //   }
    // }
    int cnt1 = check(S, 1, n, 1), cnt2 = check(T, n + 1, 2 * n, 0);
    std::cout << 1ll * cnt1 * cnt2 << "\n";
  }
  return 0;
}

B

写在最后

学到了什么:

  • I:考虑每次操作至少影响的位置口牙
  • A:大力手玩找特殊不合法情况推广口瓜
  • G:命题为真假的依赖关系的图论转化口也
  • K:双 set 支持动态维护中位数口牙
  • E:网络流的残量网络口瓜

SUA 的好题充分暴露出我这种傻逼学艺不精的本质呃呃

另外这场怎么这么卷这种题 7 题都不能稳金,妈的看来我这种傻逼不得不挑个偷鸡场了

唉下周就是武汉邀请赛了不想上这 b 大学了为啥上得和高中一样啊好相似

一轮强劲的音乐响起……这来的又是什么高手了?呱,原来是忍术研究部三位 ninja desu!南无三,简直是可爱至极口牙!恐怕是佛陀听到了都要流下热泪!

posted @ 2024-04-26 10:06  Luckyblock  阅读(85)  评论(0编辑  收藏  举报