The 2022 ICPC Asia Xian Regional Contest

Luckyblock發表於2024-09-18

目錄
  • 寫在前面
  • F 簽到
  • J 簽到
  • C 貪心,模擬
  • G 字串,雜湊
  • L 貪心,結論
  • E 數學,模擬
  • B 結論,網路流
  • A LCT or 根號預處理 or 線段樹分治維護連通性
  • D 倍增,DP
  • 寫在最後

寫在前面

比賽地址:https://codeforces.com/gym/104077

以下按個人向難度排序。

vp 8 題 900+ 罰時差 100 罰時金。

唉唉現在題數夠了還是很爽的,然而唐氏太多白白吃好多發不該。

而且感覺隊友線上時長不夠啊呃呃必須要 push 一下、、、

F 簽到

祝大秦酒店越辦越好。

不對我草今年估計 EC 西安應該能拿個名額怎麼輪到我去住大秦酒店了我草

Code by wenqizhi:

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

#define ll long long
#define ull unsigned long long

int read()
{
    int x = 0; bool f = false; 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 = 1000;
int n, c1, c2;
char s[100];
int main()
{
    n = read(), c1 = read(), c2 = read(), c1 = min(c1, c2);
    if(c1 * 2 <= c2){ printf("%lld\n", 3ll * n * c1); return 0; }
    ll ans = 0;
    for(int i = 1; i <= n; ++i)
    {
        scanf("%s", s + 1);
        int flag = 0;
        if(s[1] == s[2] || s[1] == s[3] || s[2] == s[3]) flag = 1;
        if(flag) ans += c1 + c2;
        else ans += c1 * 3;
    }
    printf("%lld\n", ans);
    return 0;
}

J 簽到

考慮列舉選的數中下標 \(i\) 的最大值,發現之後僅能在 \(1\sim i-1\) 中再選一個數,優先佇列搞一下即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  int n; std::cin >> n;
  std::priority_queue <int> q;
  
  LL ans = 0;
  for (int i = 1; i <= n; ++ i) {
    int a; std::cin >> a;
    LL sum = a;
    if (!q.empty()) sum += std::max(0, q.top());
    ans = std::max(ans, sum);
    q.push(a);
  }
  std::cout << ans << "\n";
  return 0;
}

C 貪心,模擬

發現所有人先進行復制再進行造題一定是最優的,若某輪中既有造題的又有複製的,將這輪都改成複製,則最終結束時刻一定不會更靠後。

複製時數量是倍增的,於是直接列舉複製幾輪即可。

Code by dztlb:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5;
const int inf=1e9;
int T;
int a,b,c;
signed main(){
	cin>>T;
	while(T--){
		cin>>a>>b>>c;
		int ans=inf*inf;
		for(int i=0;i<=30;++i){
			int cnt=1;
			int tmp=0;
			for(int j=1;j<=i;++j){
				cnt*=2;
				tmp+=a;
			}
			int now=(c/cnt);
			if(c%cnt!=0) ++now;
			tmp+=now*b;
			ans=min(ans,tmp);
		}
		cout<<ans<<'\n';
	}
	return 0;
}

G 字串,雜湊

顯然答案一定是給定的 \(n\) 個字串中的某個。

答案 \(s_1s_2\cdots s_{|s|}\) 的所有子串均在給定的 \(n\) 個字串中出現過,發現這等價於:\(s_2s_2\cdots s_{|s|}\)\(s_1s_2\cdots s_{|s|-1}\) 均出現過。

於是考慮把所有字串雜湊一下用於判重,然後按照長度列舉所有字串,透過上述結論判斷當前字串是否合法並標記即可。

被標記的最長的字串即為答案。

有唐氏寫雜湊忘驅魔了呃呃

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const LL p = 1e9 + 7;
const LL c = 1331;
//=============================================================
int n;
std::string s[kN];
bool yes[kN];
std::set<LL> hash;
//=============================================================
bool cmp(const std::string &fir_, const std::string &sec_) {
  return fir_.length() < sec_.length();
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  for (int i = 1; i <= n; ++ i) std::cin >> s[i];
  std::sort(s + 1, s + n + 1, cmp);

  int ans = 0;
  for (int i = 1; i <= n; ++ i) {
    int len = s[i].length();
    LL h = 0, h1 = 0, h2 = 0;
    if (len == 1) {
      yes[i] = 1;
    } else {
      for (int j = 0; j < len - 1; ++ j) h1 = (c * h1 + s[i][j]) % p;
      for (int j = 1; j < len; ++ j) h2 = (c * h2 + s[i][j]) % p;
      yes[i] = (hash.count(h1) && hash.count(h2));
    }
    h = (c * h1 + s[i][len - 1]) % p;
    if (yes[i]) ans = std::max(ans, len), hash.insert(h);
  }
  std::cout << ans << "\n";
  return 0;
}
/*
5
a
aa
aaa
aaaa
aaaaa
*/

L 貪心,結論

有唐氏寫的一坨嗯吃兩發被我狠狠批判然後五分鐘重構一發過了。

題意等價於使用極大的鏈或反鏈完全覆蓋一棵有根樹的最小覆蓋次數。

發現若使用反鏈進行覆蓋,最優的情況是每次選擇當前有根樹的所有葉節點。若不覆蓋葉節點則既不能減少鏈的數量,從而便於使用鏈覆蓋,且之後不可避免地還需要覆蓋葉節點。

葉節點至多僅有 \(O(n)\) 層,於是考慮模擬地不斷使用反鏈覆蓋,列舉刪去葉節點的層數,則之後需要的鏈覆蓋的次數即為當前葉節點數量。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e6 + 10;
//=============================================================
std::vector<int> leaves[2];
int n, fa[kN], son[kN];
//=============================================================
//=============================================================
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;
    leaves[0].clear(), leaves[1].clear();
    for (int i = 1; i <= n; ++ i) son[i] = fa[i] = 0;
    for (int i = 2; i <= n; ++ i) {
      std::cin >> fa[i];
      ++ son[fa[i]];
    }
    
    for (int i = 1; i <= n; ++ i) {
      if (son[i] == 0) leaves[0].push_back(i);
    }

    int ans = leaves[0].size(), now = 0;
    for (int i = 1; i <= n; ++ i, now ^= 1) {
      if (leaves[now].empty()) break;
      leaves[now ^ 1].clear();

      for (auto u: leaves[now]) {
        -- son[fa[u]];
        if (son[fa[u]] == 0) leaves[now ^ 1].push_back(fa[u]);
      }
      ans = std::min(ans, (int) (i + leaves[now ^ 1].size()));
    }
    std::cout << ans << "\n";
  }
  return 0;
}
/*
3
7
1 1 2 2 2 3
5
1 2 3 4
11
1 2 3 4 5 2 3 4 5 6
*/

E 數學,模擬

發現給定式子相當於考慮 \(x\) 的三進製表示,\(f(x)\) 即三進位制下的位數+三進位制各位之和。

於是大力討論即可。最優情況是各位全部取 2,需要討論被上下界限制時取不到最優情況。

Code by wenqizhi:

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

#define ll long long
#define ull unsigned long long

ll read()
{
    ll x = 0; bool f = false; 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 = 105;
ll sum[N];

void init()
{
    sum[0] = 1;
    for(int i = 1; i; ++i)
    {
        sum[i] = sum[i - 1] * 3;
        if(sum[i] > 1e18) break;
    }
}

int get(int len, ll r)
{
    ll L = sum[len - 1];
    int cnt = 1;
    for(int i = 1; i <= len; ++i)
    {
        for(int j = 1; j <= 2 - (i == len); ++j)
            if(sum[i - 1] + L <= r) ++cnt, L += sum[i - 1];
    }
    return cnt;
}

void solve()
{
    ll l = read(), r = read(), L = l;
    int ans = 0;
    int len = 0, cnt = 0;
    while(L)
    {
        cnt += L % 3;
        L /= 3;
        ++len;
    }
    L = l;
    len = max(len, 1);
    ans = max(ans, cnt);
    for(int i = 1; i <= len; ++i)
    {
        int res = 3 - l / sum[i - 1] % 3 - 1;
        for(int j = 1; j <= res; ++j)
            if(sum[i - 1] + L <= r) ++cnt, L += sum[i - 1];
    }
    ans = max(ans, cnt + len);
    for(int i = len + 1; i; ++i)
    {
        if(sum[i - 1] > r || sum[i - 1] == 0) break;
        ans = max(ans, get(i, r) + i);
    }
    printf("%d\n", ans);
}

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

B 結論,網路流

我去這題給 dztlb 大神開出來了真是我疊吧

根據給定的變數的含義,顏色數量 \(k\) 與填 0 數量 \(z\) 顯然成反比關係,則容易看出 \(ck+dz\) 是關於 \(k\) 的單谷函式。於是考慮三分,檢查在使用 \(k\) 種顏色情況下,最少填多少 0(最多可以填多少非 0)可以合法,即可求得對應的 \(ck+dz\) 的值。

發現問題可以看做是在給定顏色種類數限制下,保證每行/每列同種非 0 顏色數量不大於 1,最大化填的非 0 的數量,又資料範圍很小,一個顯然的想法是透過網路流進行非 0 的填色最大化流量,跑最大流即為所求。

對於上述問題,記三分量為 \(\operatorname{mid}\),dztlb 大神的建圖策略如下:

  • 先統計每行/每列的空格數量,記為 \(\operatorname{cntx}, \operatorname{cnty}\)
  • 對每行/每列建點,編號分別為 \(1\sim n\)\(n + 1\sim n + m\)
  • 對於第 \(i\) 行,若有 \(\operatorname{cntx}_i\ge \operatorname{mid}\),連邊 \((S, i, \operatorname{cntx}_i - \operatorname{mid})\),然後列舉列 \(j\),若有 \(\operatorname{cnty}_j\ge \operatorname{mid}\),連邊 \((i, n+j, 1)\)
  • 對於第 \(j\) 列,若有 \(\operatorname{cnty}_j\ge \operatorname{mid}\),連邊 \((n+j, T, \operatorname{cnty}_j - \operatorname{mid})\)
  • 則最小化填 0 數量即:\(\sum \operatorname{cntx} + \sum \operatorname{cnty} - \operatorname{maxflow}\)

建邊數量為 \(O(nm)\) 級別,但是這圖只有 4 層根本跑不滿,實際執行效率非常高。

然後我當程式碼黑奴給 dztlb 大神寫了最大流寫了三分就過了哈哈

//知識點:網路最大流,Dinic
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const int kM = 2e6 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, m, c, d, S, T;
int nodenum, maxnodenum, edgenum = 1, v[kM], ne[kM], head[kN];
int cur[kN], dep[kN];
std::string map[kN];
int cn[kN],cm[kN];
LL w[kM];
//=============================================================
void addedge(int u_, int v_, LL w_) {
  v[++ edgenum] = v_;
  w[edgenum] = w_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;

  v[++ edgenum] = u_;
  w[edgenum] = 0;
  ne[edgenum] = head[v_];
  head[v_] = edgenum;
}
void init() {
  edgenum = 1;
  nodenum = n + m;
  maxnodenum = 3 * n + 2;
  S = ++ nodenum, T = ++ nodenum;
  
  for (int i = 1; i <= maxnodenum; ++ i) {
    head[i] = 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 > 1; i = ne[i]) {
      int v_ = v[i], w_ = w[i];
      if (w_ > 0 && !dep[v_]) {
        dep[v_] = dep[u_] + 1;
        q.push(v_);
      }
    }
  }
  return dep[T];
}
LL dfs1(int u_, LL into_) {
  if (u_ == T) return into_; 
  LL ret = 0;
    for (int i = cur[u_]; i > 1 && into_; i = ne[i]) {
    int v_ = v[i];
    LL w_ = w[i];
    if (w_ && dep[v_] == dep[u_] + 1) {
            LL 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;
}
LL dinic() {
  LL ret = 0;
  while (bfs()) {
    memcpy(cur, head, (nodenum + 1) * sizeof (int));
    ret += dfs1(S, kInf);
  }
  return ret;
}
LL check(LL mid_) {
  init();
  LL tmp=0;
	for(int i=1;i<=n;++i){
		if(cn[i]>mid_){
			addedge(S,i,cn[i]-mid_);
			tmp+=cn[i]-mid_;
			for(int j=1;j<=m;++j){
				if(cm[j]>mid_){
					addedge(i,n+j,1);
				}
			}
		}
	}
  for(int i=1;i<=m;++i){
		if(cm[i]>mid_){
			addedge(n+i,T,cm[i]-mid_);
			tmp+=cm[i]-mid_;
		}
	}
	return 1ll * mid_ * c + d * (tmp - dinic());
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n >> m >> c >> d;
  for (int i = 1; i <= n; ++ i) {
    std::cin >> map[i]; map[i] = "$" + map[i];
  }
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			if(map[i][j]=='.'){
				cn[i]++,cm[j]++;
			}
		}
	}

  LL lmid, rmid, lans = kInf, rans = kInf;
  for (LL l = 0, r = std::max(n, m); l <= r; ) {
    lmid = l + (r - l) / 3;
    rmid = r - (r - l) / 3;
    lans = check(lmid), rans = check(rmid);
    if (rans <= lans) l = lmid + 1;
    else r = rmid - 1;
  }
  std::cout << std::min(lans, rans) << "\n";
  return 0;
}
/*
5 5 3 1
*..*.
...**
.*...
..*..
..*..
*/

A LCT or 根號預處理 or 線段樹分治維護連通性

我去這題給我開出來了最後十分鐘一發過了我真是疊吧

保證一個點至多連一條橋,則從每個起點出發,到達的終點一定是不同且唯一的。

發現在 \((a, b), (a + 1, b)\) 連邊,對移動路徑的影響,等價於斷開邊:\((a, b)\rightarrow (a, b + 1), (a+1, b)\rightarrow (a+1, b + 1)\),然後連邊:\((a, b)\rightarrow (a+1, b + 1), (a+1, b)\rightarrow (a, b + 1)\)

發現斷邊連邊後,給定的圖仍是 \(n\) 條長度為 \(m+1\) 的鏈,一個顯然的想法是使用 LCT 直接維護,考慮僅令每條鏈結尾的點權值為其鏈號,則查詢僅需查詢給定起點對應鏈的樹的權值和即可。

然而有 \(O(nm)\) 個點並不能直接做,但是發現至多隻有 \(q\) 次操作,則有影響的點僅有 \(O(n+q)\) 級別,於是考慮將每行的點按是否連了橋進行分段,則每段都可以直接縮成一個點考慮,就能直接上 LCT 了。

我的分段縮點方法是先將每條鏈看做一個點,然後模擬連橋後將原來的點分裂得到新點,比較唐呃呃,優美做法可見 HolyK 的這發提交:https://codeforces.com/gym/104077/submission/183684508

std 是根號預處理;此外還有大神的線段樹分治維護連通性,可見這發提交:https://codeforces.com/gym/104077/submission/272665032

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e5 + 10;
//=============================================================
int n, m, q, a[kN], ans[kN];
struct O {
  int opt, a, b;
} op[kN];
struct Node {
  int id, a, l, r, val;
  bool operator <(const Node& sec_) const {
    if (a != sec_.a) return a < sec_.a;
    if (r != sec_.r) return r < sec_.r;
    if (l != sec_.l) return l < sec_.l;
    if (id != sec_.id) return id < sec_.id;
    return val < sec_.val;
  } 
} node[kN];
int nodenum, next[kN];
std::set<Node> st;
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
namespace LCT {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  int fa[kN], son[kN][2];
  int val[kN], sum[kN], sum1[kN];
  bool rev[kN];

  void clear(int now_) {
    ls = rs = f = val[now_] = 0;
  }
  void Pushup(int now_) {
    sum[now_] = (sum[ls] + sum[rs] + val[now_] + sum1[now_]);
  }
  void Rev(int now_) {
    if (!now_) return ;
    std::swap(ls, rs);
    rev[now_] ^= 1;
  }
  void Pushdown(int now_) {
    bool rev_ = rev[now_];
    if (rev_) Rev(ls), Rev(rs);
    rev[now_] = 0;
  }
  bool Isroot(int now_) {
    return son[f][0] != now_ && son[f][1] != now_;
  }
  bool Whichson(int now_) {
    return son[f][1] == now_;
  }
  void Rotate(int now_) {
    int fa_ = f, ffa = fa[f], w = Whichson(now_);
    if (!Isroot(f)) son[fa[f]][Whichson(f)] = now_;
    f = fa[f];

    son[fa_][w] = son[now_][w ^ 1];
    fa[son[fa_][w]] = fa_;

    son[now_][w ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_), Pushup(ffa);
  }
  void Update(int now_) {
    if (!Isroot(now_)) Update(f);
    Pushdown(now_);
  }
  void Splay(int now_) {
    Update(now_);
    for (; !Isroot(now_); Rotate(now_)) {
      if (!Isroot(f)) Rotate(Whichson(f) == Whichson(now_) ? f : now_);
    }
  }

  void Access(int now_) {
    for (int last = 0; now_; last = now_, now_ = f) {
      Splay(now_); 
      sum1[now_] += sum[rs] - sum[last];
      rs = last;
      Pushup(now_);
    }
  }
  void Makeroot(int now_) {
    Access(now_);
    Splay(now_);
    Rev(now_);
  }
  int Find(int now_) {
    Access(now_);
    Splay(now_);
    while (ls) now_ = ls;
    Splay(now_);
    return now_;
  }
  void Split(int x_, int y_) {
    Makeroot(x_);
    Access(y_);
    Splay(y_);
  }
  void Link(int x_, int y_) {
    Makeroot(x_);
    // fa[x_] = y_;
    if (Find(y_) != x_) fa[x_] = y_;
  }
  void Cut(int x_, int y_) {
    // Split(x_, y_);
    // fa[x_] = son[y_][0] = 0;
    // Pushup(y_);
    Makeroot(x_);
    if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
    fa[y_] = son[x_][1] = 0;
    Pushup(x_);
  }
  
  int query(int x_) {
    Makeroot(x_);
    return sum[x_];
  }

  void Init() {
    for (int i = 1; i <= nodenum; ++ i) {
      val[i] = sum[i] = node[i].val;
    }
    for (int i = 1; i <= nodenum; ++ i) {
      if (next[i]) Link(i, next[i]);
    }
  }
}
void Init() {
  n = read(), m = read(), q = read();
  
  nodenum = n;
  for (int i = 1; i <= n; ++ i) {
    node[i] = (Node) {i, i, 1, m + 1, i};
    st.insert(node[i]);
  }
  for (int i = 1; i <= q; ++ i) {
    int opt = read();
    if (opt == 1) {
      int a = read(), b = read();

      Node u1 = *st.lower_bound((Node) {0, a, 0, b, 0});
      Node u2 = *st.lower_bound((Node) {0, a + 1, 0, b, 0});
      node[++ nodenum] = (Node) {nodenum, a, b + 1, u1.r, u1.val};
      node[++ nodenum] = (Node) {nodenum, a + 1, b + 1, u2.r, u2.val};
      node[u1.id].r = node[u2.id].r = b;
      node[u1.id].val = node[u2.id].val = 0;

      st.erase(st.lower_bound(u1)), st.erase(st.lower_bound(u2));
      st.insert(node[u1.id]), st.insert(node[u2.id]);
      st.insert(node[nodenum - 1]), st.insert(node[nodenum]);
      /*
      struct Node {
        int id, a, l, r, val;
      */
      op[i] = (O) {1, a, b};
    } else {
      int a = read();
      op[i] = (O) {2, a, 0};
    }
  }
  for (std::set<Node>::iterator it = st.begin(); it != st.end(); ++ it) {
    std::set<Node>::iterator it1 = it;
    ++ it1;
    if (it1 == st.end()) break;
    if (it->a != it1->a) continue;
    next[it->id] = it1->id;
  }
  LCT::Init();
}
void modify(int x_, int y_) {
  int u1 = st.lower_bound((Node) {0, x_, 0, y_, 0})->id;
  int u2 = st.lower_bound((Node) {0, x_ + 1, 0, y_, 0})->id;

  LCT::Cut(u1, next[u1]), LCT::Cut(u2, next[u2]);
  std::swap(next[u1], next[u2]);
  LCT::Link(u1, next[u1]), LCT::Link(u2, next[u2]);
}
int query(int x_) {
  return LCT::query(st.lower_bound((Node) {0, x_, 0, 0, 0})->id);
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  Init();
  for (int i = 1; i <= q; ++ i) {
    if (op[i].opt == 2) {
      printf("%d\n", query(op[i].a));
    } else {
      modify(op[i].a, op[i].b);
    }
  }
  return 0;
}

D 倍增,DP

唉唉這不比 A 簡單?

寫在最後

學到了什麼:

  • A:轉化為經典資料結構問題;LCT 透過維護虛子樹,維護可加性的子樹資訊。
  • B:雙變數函式,但是兩個變數有線性關係——則雙變數函式對於單變數為單峰/單谷。

然後是日常的夾帶私貨環節:

相關文章