The 2021 ICPC Asia Shenyang Regional Contest

Luckyblock發表於2024-10-11

目錄
  • 寫在前面
  • E 簽到
  • F 簽到
  • J BFS
  • B 帶權並查集
  • H 圖論
  • I 數學
  • L 樹形DP,容斥
  • M 字串,離線,單調性
  • G 貪心
  • 寫在最後

寫在前面

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

以下按個人向難度排序。

唉唉國慶 vp 三場唯一打的還像人的一場,最後手裡還有兩道題在寫可惜都沒出來呃呃。

被樹上揹包沙勒呃呃呃要苦學樹上揹包!

E 簽到

我看都沒看。

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 = 2e5 + 5;
char s[N];

int main()
{
    scanf("%s", s + 1);
    int cnt = 0, len = strlen(s + 1);
    for(int i = 1; i <= len - 4; ++i)
    {
        if(s[i] == 'e' && s[i + 1] == 'd' && s[i + 2] == 'g' && s[i + 3] == 'n' && s[i + 4] == 'b') ++cnt;
    }  
    printf("%d\n", cnt);
    return 0;
}

F 簽到

直接大力列舉字尾暴力做然後大力排序即可。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1010;
//=============================================================
int n, cnt[21], map[21]; 
std::string s, ans[kN];
//=============================================================
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n;
  std::cin >> s; s = "$" + s;
  for (int i = 1; i <= n; ++ i) {
    int num = 0;
    for (int j = 0; j < 20; ++ j) cnt[j] = 0, map[j] = 0;
    for (int j = i; j; -- j) {
      if (cnt[s[j] - 'a'] == 0) map[s[j] - 'a'] = num, ++ num;
      ++ cnt[s[j] - 'a'];
    }
    for (int j = 1; j <= i; ++ j) ans[i].push_back(map[s[j] - 'a'] + 'a');
  }
  std::sort(ans + 1, ans + n + 1);
  std::cout << ans[n] << "\n";
  return 0;
}

J BFS

dztlb 大爹覺得一眼秒了但是唐了 1h 吃了三發都沒出我 tama 受不了了直接重新開了一遍這題發現純傻逼題 10min 過了呃呃

顯然僅需考慮起點和終點每位在環上的相對差值即可,即對於詢問 \((s, t)\) 等價於詢問 \((0, t - s)\)

於是僅需預處理起點為 \(0\) 時,到所有狀態的最短路長度,邊權值均為 1,考慮 BFS 實現即可。

狀態數量僅有 \(10^4\) 個,每個狀態轉移時僅需大力列舉操作的區間和方向即可,轉移僅有 32 種,則總轉移次數不超過 \(10^6\) 次。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int dis[kN];
bool vis[kN];
//=============================================================
int trans(int u_, int l_, int r_, int v_) {
  int v = 0;
  for (int i = 0, pw10 = 1; i < 4; ++ i, pw10 *= 10) {
    if (i < l_ || r_ < i) {
      v += (u_ % 10) * pw10;
    } else {
      v += (u_ % 10 + v_ + 10) % 10 * pw10;
    }
    u_ /= 10;
  }
  return v;
}
void init() {
  for (int i = 0; i < 10000; ++ i) dis[i] = kInf;
  std::queue<int> q;
  q.push(0);
  dis[0] = 0;
  vis[0] = 1;

  while (!q.empty()) {
    int u = q.front(); q.pop();

    for (int l = 0; l < 4; ++ l) {
      for (int r = l; r < 4; ++ r) {
        int v = trans(u, l, r, 1);
        if (!vis[v]) vis[v] = 1, dis[v] = dis[u] + 1, q.push(v);
        v = trans(u, l, r, -1);
        if (!vis[v]) vis[v] = 1, dis[v] = dis[u] + 1, q.push(v);
      }
    }
  }
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  init();
  
  int T; std::cin >> T;
  while (T --) {
    std::string a, b; std::cin >> a >> b;
    int t = 0;
    for (int i = 0; i < 4; ++ i) t = 10 * t + (b[i] - a[i] + 10) % 10;

    // std::cout << t << "\n";
    std::cout << dis[t] << "\n";
  }
  return 0;
}

B 帶權並查集

wenqizhi 大爹秒了。

考慮將每個數看做一個點,發現給定的限制關係實際上構成了若干連通塊,不同連通塊內互不影響,僅需考慮每個連通塊內如何構造即可。

一開始的想法是考慮類似 2-SAT 的拆位,但是發現並不需要。發現對於某個連通塊內,只需要確定某一個數的取值,其他所有數的取值即可唯一確定,於是考慮帶權並查集維護聯通塊內每個數到根節點的異或和即可,手玩下容易得到合併聯通塊時對權值的影響。

然後再列舉每個聯通塊,拆位統計貢獻,根據帶權並查集的資訊,貪心確定該聯通塊的根節點這一位的取何值時,這一位貢獻和最小即可。

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 = 1e5 + 5;
int f[N], xo[N], n, m;

int find(int x)
{
    if(x == f[x]) return x;
    int fa = f[x];
    f[x] = find(f[x]);
    xo[x] ^= xo[fa];
    return f[x];
}

vector<int> num[N];
int cnt[100];

int main()
{
    n = read(), m = read();
    for(int i = 1; i <= n; ++i) f[i] = i;
    for(int i = 1; i <= m; ++i)
    {
        int u = read(), v = read(), w = read();
        int fu = find(u), fv = find(v);
        if(fv != fu)
        {
            xo[fv] ^= xo[v] ^ xo[u] ^ w, f[fv] = fu;
        }else
        {
            if((xo[u] ^ xo[v]) != w)
            {
                printf("-1\n");
                return 0;
            }
        }
    }
    for(int i = 1; i <= n; ++i)
    {
        find(i);
        if(i != f[i]) num[f[i]].emplace_back(xo[i]);
    }
    ll ans = 0;
    for(int t = 1; t <= n; ++t)
        if(num[t].size())
        {
            ll tot = num[t].size();
            for(int i = 0; i <= 29; ++i) cnt[i] = 0;
            for(int i = 0; i < tot; ++i)
            {
                int x = num[t][i];
                for(int j = 0; j <= 29; ++j)
                    if((1 << j) & x) ++cnt[j];
            }
            for(int i = 29; i >= 0; --i) ans += min((1ll << i) * cnt[i], (1ll << i) * (tot - cnt[i] + 1));
        }
    printf("%lld\n", ans);
    return 0;
} 

H 圖論

dztlb 大爹看了眼秒了但是鑑於他太唐了不敢讓他上機於是我寫的。

這是什麼?這是線圖。容易發現線圖的匹配就是原圖的邊匹配,當原圖兩條邊有公共點時即可匹配,貢獻即兩條邊權之和。手玩下發現當原圖邊的數量為偶數時,總能全部匹配完並取得全部貢獻。

當原圖邊數量為奇數時,則最優方案一定是一定僅有一條邊無法匹配。若存在僅有某條邊無法匹配的方案,則刪掉這條邊後一定是得到一張邊數為偶數的連通圖,或是邊數為偶數的兩個聯通塊。則一條邊不合法的情況僅為:

  • 是割邊;
  • 該邊連線的兩個聯通塊內的邊數均為奇數。

考慮邊雙縮點,求得割邊並維護邊雙內邊的數量。顯然邊雙縮點後得到的是一棵樹,樹邊均為割邊,在這棵樹上 dfs 考慮斷邊後子樹內邊的數量是否為奇數即可。

稍微有點難寫呃呃。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e5 + 10;
const int kM = 2e6 + 10;
//=============================================================
int n, m;
int edgenum = 1, head[kN], v[kM], w[kM], ne[kM];
int dfnnum, dfn[kN], low[kN];
bool bridge[kM];
int dccnum, indcc[kN], sz[kN];
std::vector<pr <int, int> > edge[kN];

LL ans, edgesum;
bool tag[kM];
//=============================================================
void addedge(int u_, int v_, int w_) {
  v[++ edgenum] = v_;
  w[edgenum] = w_;
  ne[edgenum] = head[u_];
  head[u_] = edgenum;
}
void tarjan(int u_, int from_) {
  dfn[u_] = low[u_] = ++ dfnnum;
  for (int i = head[u_]; i > 1; i = ne[i]) {
    int v_ = v[i];
    if (!dfn[v_]) {
      tarjan(v_, i);
      if (low[v_] > dfn[u_]) bridge[i] = bridge[i ^ 1] = 1;
      low[u_] = std::min(low[u_], low[v_]);
    } else if (i != (from_ ^ 1)) {
      low[u_] = std::min(low[u_], dfn[v_]);
    }
  }
}
void dfs(int u_, int id_) {
  indcc[u_] = id_;
  for (int i = head[u_]; i > 1; i = ne[i]) {
    int v_ = v[i];
    if (indcc[v_] || bridge[i]) continue;
    dfs(v_, id_);
  }
}
void dfs1(int u_, int fa_) {
  for (auto [v_, i]: edge[u_]) {
    if (v_ == fa_) continue;
    dfs1(v_, u_);
    if (sz[v_] % 2 == 1 && (m - sz[v_] - 1) % 2 == 1) {
      tag[i] = 1;
    }
    sz[u_] += sz[v_] + 1;
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> n >> m;
  for (int i = 1; i <= m; ++ i) {
    int u_, v_, w_; std::cin >> u_ >> v_ >> w_;
    edgesum += w_;
    addedge(u_, v_, w_), addedge(v_, u_, w_);
  }
  if (m % 2 == 0) {
    std::cout << edgesum << "\n";
    return 0;
  }

  for (int i = 1; i <= n; ++ i) if (!dfn[i]) tarjan(i, 0);
  for (int i = 1; i <= n; ++ i) if (!indcc[i]) dfs(i, ++ dccnum);
  for (int u_ = 1; u_ <= n; ++ u_) {
    for (int i = head[u_]; i > 1; i = ne[i]) {
      if (i % 2 == 1) continue;
      int v_ = v[i];
      if (indcc[u_] == indcc[v_]) {
        ++ sz[indcc[u_]];
        continue;
      }
      edge[indcc[u_]].push_back(mp(indcc[v_], i));
      edge[indcc[v_]].push_back(mp(indcc[u_], i));
    }
  }
  dfs1(1, 0);
  for (int i = 2; i <= 2 * m; i += 2) {
    if (!tag[i]) ans = std::max(ans, edgesum - w[i]);
  }
  std::cout << ans << "\n";
  return 0;
}

I 數學

wenqizhi 大爹真是大爹啊我草,大力解方程過了。

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 double eps = 1e-10;

struct node
{
    double A, B;
    node(){ A = 0, B = 0; }
    node friend operator + (node a, node b)
    {
        node c;
        c.A = a.A + b.A;
        c.B = a.B + b.B;
        return c;
    }
    node friend operator - (node a, node b)
    {
        node c;
        c.A = a.A - b.A;
        c.B = a.B - b.B;
        return c;
    }
    node friend operator * (node a, node b)
    {
        node c;
        c.A = a.A * b.A - a.B * b.B;
        c.B = a.A * b.B + a.B * b.A;
        return c;
    }
    node friend operator / (node a, node b)
    {
        node c, d;
        d.A = b.A, d.B = -b.B;
        double x = b.A * b.A + b.B * b.B;
        d.A /= x, d.B /= x;
        return a * d;
    }
}z1, z2, z3, z0, w1, w2, w3, w0, X, Y, K, ans, I;

bool check(node a)
{
    double x = sqrt(a.A * a.A + a.B * a.B );
    if(fabs(x) < eps) return false;
    return true;
}

void solve()
{
    I.A = 1, I.B = 0;
    scanf(" %lf %lf %lf %lf", &z1.A , &z1.B , &w1.A , &w1.B );
    scanf(" %lf %lf %lf %lf", &z2.A , &z2.B , &w2.A , &w2.B );
    scanf(" %lf %lf %lf %lf", &z3.A , &z3.B , &w3.A , &w3.B );
    scanf(" %lf %lf", &z0.A , &z0.B );
    if(check(((z2 - z1) * (w3 * z3 - w1 * z1) - (z3 - z1) * (w2 * z2 - w1 * z1))) && check(z3 - z1))
    {
        // printf("DDD\n");
        X = ((z3 - z1) * (w2 - w1) - (z2 - z1) * (w3 - w1)) / ((z2 - z1) * (w3 * z3 - w1 * z1) - (z3 - z1) * (w2 * z2 - w1 * z1));
        Y = ((w3 * z3 - w1 * z1) * X + w3 - w1) / (z3 - z1);
        K = w1 * z1 * X + w1 - z1 * Y;
        ans = (Y * z0 + K) / (X * z0 + I);
        printf("%.10lf %.10lf\n", ans.A , ans.B );
        return ;
    }
    if(check(((z3 - z1) * (w2 - w1) - (z2 - z1) * (w3 - w1))) && check(z3 - z1))
    {
        // printf("CCC\n");
        X = ((z2 - z1) * (w3 * z3 - w1 * z1) - (z3 - z1) * (w2 * z2 - w1 * z1)) / ((z3 - z1) * (w2 - w1) - (z2 - z1) * (w3 - w1));
        Y = (w3 * z3 - w1 * z1 + (w3 - w1) * X) / (z3 - z1);
        K = w1 * z1 + w1 * X - z1 * Y;
        ans = (Y * z0 + K) / (z0 + X);
        printf("%.10lf %.10lf\n", ans.A , ans.B );
        return ;
    }
    if(check(((w2 - w1) * (w3 * z3 - w1 * z1) - (w3 - w1) * (w2 * z2 - w1 * z1))) && check(w2 - w1))
    {
        // printf("AAA\n");
        X = ((w2 - w1) * (z3 - z1) - (w3 - w1) * (z2 - z1)) / ((w2 - w1) * (w3 * z3 - w1 * z1) - (w3 - w1) * (w2 * z2 - w1 * z1));
        Y = ((z2 - z1) - (w2 * z2 - w1 * z1) * X) / (w2 - w1);
        K = w1 * z1 * X + w1 * Y - z1;
        ans = (z0 + K) / (X * z0 + Y);
        printf("%.10lf %.10lf\n", ans.A , ans.B );
        return ;
    }
    if(check(((z1 * z3 * w1 - z1 * z3 * w3) * (z1 * w2 - z2 * w1) - (z1 * z2 * w1 - z1 * z2 * w2) * (z1 * w3 - z3 * w1))) && check((z1 * z2 * w3 - z1 * z3 * w1)) && check(z1))
    {
        // printf("BBB\n");
        X = ((z1 * z2 * w1 - z1 * z2 * w2) * (z3 - z1) - (z1 * z3 * w1 - z1 * z3 * w3) * (z2 - z1)) / ((z1 * z3 * w1 - z1 * z3 * w3) * (z1 * w2 - z2 * w1) - (z1 * z2 * w1 - z1 * z2 * w2) * (z1 * w3 - z3 * w1));
        Y = (z3 * w1 * X - z1 * w3 * X + z1 - z3) / (z1 * z2 * w3 - z1 * z3 * w1);
        K = (w1 * z1 * Y + w1 * X - I) / (z1);
        ans = (K * z0 + I) / (Y * z0 + X);
        printf("%.10lf %.10lf\n", ans.A , ans.B );
        return ;
    }

}

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

L 樹形DP,容斥

賽時想到樹形DP但是狀態假了呃呃。


M 字串,離線,單調性

草這題做過詢問區間任意的版本而且打算出到新生賽上,於是直接拿過來秒了。

題解也搬一下:

一個顯然的性質是子串 \(s[l:r]\) 中字典序最大的子串的右端點一定是 \(r\)

考慮將所有詢問 \((l, r)\) 離線,按右端點 \(r\) 升序排序後順序列舉右端點 \(r\),則此時所有詢問的答案一定以 \(r\) 結尾。在此過程中維護以 \(r\) 結尾的所有子串 \(s[1:r] \sim s[r:r]\) 的字典序大小關係並回答詢問。

發現對於某個確定的右端點 \(r\),當詢問左端點 \(l\) 增加時,作為答案的子串 \(s[p:r]\) 的左端點一定是單調不降的,字典序一定是單調不增的。則考慮在列舉 \(r\) 的同時,對有貢獻的左端點,維護一個按照答案的左端點 \(p\) 遞增字典序遞減的序列,回答詢問時二分即得到答案。

對於兩個子串 \(s[p_1: r], s[p_2: r](p_1 < p_2)\),若有 \(s[p_1: r]<s[p_2: r]\) 則當 \(r\) 增加時恆有 \(s[p_1: r']<s[p_2: r']\) 成立,又 \([p_2, r]\in [p_1, r]\)\(s[p_1: r']\) 對之後的任何詢問均無貢獻,可以將其刪去。

更一般地,記 \(\operatorname{lcp}(s_1, s_2)\) 表示兩個字串 \(s_1, s_2\) 的最長公共字首的長度,對於兩個原字串的字尾 \(s[p_1, n], s[p_2, n](p_1 < p_2)\),記 \(\operatorname{lcp} = \operatorname{lcp}(s[p_1:n], s[p_2:n])\)

  • \(s_{p_1 + \operatorname{lcp}}>s_{p_2 + \operatorname{lcp}}\),則恆有 \(s[p_1:r] > s[p_2:r]\)
  • \(s_{p_1 + \operatorname{lcp}}<s_{p_2 + \operatorname{lcp}}\),則當 \(r<p_2 + \operatorname{lcp}\) 時有 \(s[p_1:r] > s[p_2:r]\);當 \(r\ge p_2 + \operatorname{lcp}\) 時有 \(s[p_1:r] < s[p_2:r]\)。則在列舉到 \(r=p_2 + \operatorname{lcp}\)\(s[p_1:r]\) 對之後的任何詢問都無貢獻,可以將其刪去。
  • 對於上述情況,我們稱左端點 \(p_2\) 支配了左端點 \(p_1\)。發現支配關係實際上構成了一個 DAG 的結構,當左端點 \(p_1\) 被刪除時,顯然被 \(p_1\) 支配的所有左端點之後也不會有貢獻,應當遞迴地把它們全部刪除。

考慮在上述過程中,額外維護此時沒有被支配的左端點序列,用來輔助有貢獻的左端點的維護。顯然該序列也是有上述按照答案的左端點 \(p\) 遞增,字典序遞減的單調性的。為了方便刪除均使用 set 實現即可。

在列舉右端點時維護 \(\operatorname{D}_r\) 表示列舉到 \(r\)應刪除哪些左端點,\(\operatorname{G}_p\) 表示左端點 \(p\) 直接支配哪些左端點。每次右端點 \(r\) 增加時,不斷比較序列尾部元素 \(p_1\) 與當前入棧元素 \(r\)\(s_{p_1 + \operatorname{lcp}}, s_{r+ \operatorname{lcp}}\),若有 \(s_{p_1 + l}< s_{r + l}\) 說明 \(p_1\)\(i\) 支配而應當將 \(p_1\) 彈出並更新 \(\operatorname{D}_{r+\operatorname{lcp}}\)\(\operatorname{G}_{r}\);若有 \(s_{p_1 + l}> s_{r + l}\) 由單調性可知此時序列內任何左端點均不會被 \(r\) 支配,可以結束列舉。

然後根據此時的 \(\operatorname{D}_r\)\(\operatorname{G}\) 遞迴刪除無貢獻的左端點,再列舉詢問二分即可。

那麼如何求兩個串的 \(\operatorname{lcp}\)?同樣考慮二分兩個子串長度相同的字首的長度並雜湊檢查,即可在 \(O(\log n)\) 時間複雜度內解決。當然如果你是字串大神直接掏出字尾陣列+ST表就秒了哈哈。

考慮到大家應該都不會字尾陣列所以本題 std 使用了雜湊二分實現。總複雜度 \(O((|s|+m)\log^2 |s|)\) 級別。

參考:https://blog.csdn.net/C20181220_xiang_m_y/article/details/95516076

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
const LL c1 = 29;
const LL c2 = 1145141;
const LL p1 = 1e9 + 7;
const LL p2 = 998244353;
//=============================================================
int n;
LL pw1[kN], pw2[kN], h1[kN], h2[kN];
std::string s;
std::vector<int> st, del[kN], g[kN];
std::set<int> suf;
//=============================================================
LL hash1(int l_, int r_) {
  return (h1[r_] - h1[l_ - 1] * pw1[r_ - l_ + 1] % p1 + p1) % p1;
}
LL hash2(int l_, int r_) {
  return (h2[r_] - h2[l_ - 1] * pw2[r_ - l_ + 1] % p2 + p2) % p2;
}
bool equal(int l1_, int r1_, int l2_, int r2_) {
  return hash1(l1_, r1_) == hash1(l2_, r2_) &&  
         hash2(l1_, r1_) == hash2(l2_, r2_);
}
int lcp(int i_, int j_) {
  int ret = 0;
  for (int l = 1, r = std::min(n - i_, n - j_) + 1; l <= r; ) {
    int mid = (l + r) >> 1;
    if (equal(i_, i_ + mid - 1, j_, j_ + mid - 1)) {
      ret = mid;
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
  return ret;
}
void dfs(int u_) {
  if (suf.find(u_) == suf.end()) return ;
  suf.erase(u_);
  for (auto v_: g[u_]) dfs(v_);
}
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  std::ios::sync_with_stdio(0), std::cin.tie(0);
  std::cin >> s; n = s.length(); s = "$" + s;
  pw1[0] = pw2[0] = 1;
  for (int i = 1; i <= n; ++ i) {
    pw1[i] = c1 * pw1[i - 1] % p1;
    pw2[i] = c2 * pw2[i - 1] % p2;
    h1[i] = (c1 * h1[i - 1] + s[i]) % p1;
    h2[i] = (c2 * h2[i - 1] + s[i]) % p2;
  }

  for (int i = 1; i <= n; ++ i) {
    while (!st.empty()) {
      int j = st.back(), len = lcp(i, j);
      if (s[j + len] > s[i + len]) break;
      del[i + len].push_back(j);
      g[i].push_back(j);
      st.pop_back();
    }
    st.push_back(i), suf.insert(i);
    for (auto j: del[i]) dfs(j);
    
    int p = *suf.lower_bound(1);
    std::cout << p << " " << i << '\n';
  }
  return 0;
}

G 貪心

寫在最後

學到了什麼:

  • L:

相關文章