2024“釘耙程式設計”中國大學生演算法設計超級聯賽(8)

Luckyblock發表於2024-08-13

目錄
  • 寫在前面
  • 1004
  • 1007
  • 1012
  • 1006
  • 1005
  • 1008
  • 1003
  • 1010
  • 寫在最後

寫在前面

補提地址:https://acm.hdu.edu.cn/listproblem.php?vol=66,題號 7517~7528。

以下按個人向難度排序。

dztlb 大神回去和npy約會了,於是悲慘單刷。

最後 6 題好歹簽到是都簽完了不算太爛,唉一個人單刷前期看到大家飛速過完好多題自己簽到都籤不上實在紅溫!

1004

簽到。

發現操作後一定會移動到最外一圈的格子上,且之後只能在最外圈的格子上移動,則這些格子的貢獻一定都能取到。

然後考慮能否獲得其他格子的貢獻,發現可以透過在開始時進行反覆橫跳,從而多獲得某一行/某一列的貢獻。

特判下即可。

//
/*
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 T; std::cin >> T;

  while (T --) {
    LL n, m, a, b; std::cin >> n >> m >> a >> b;
    LL ans = 0;
    if (n == 1 || m == 1) ans = n * m;
    else ans = 2 * n + 2 * m - 4;
    
    if ((a == 1 && b == 1) || (a == 1 && b == 1) || (a == n && b == 1) || (a == n && b == m)) {
      //hina daisuki!!!
    } else if (a == 1 || a == n) {
      ans += (n - 2);
    } else if (b == 1 || b == m) {
      ans += (m - 2);
    } else {
      ans += std::max(n - 2, m - 2);
    }
    std::cout << ans << "\n";
  }
  return 0;
}

1007

進位制,數論。

先考慮何時會出現答案無限的情況,考慮 \(k\) 極大的情況可知,這是因為在任意進位制下 \(a+b\) 均不發生進位導致的。此時一定有 \(a+b = c\),特判下即可。

則某個進位制 \(k\) 對答案有貢獻當且僅當此時出現了進位,且減去進位 \(d\) 後有 \(a+b - d = c\)。眾所周知,異或作為二進位制不進位加法是有個性質對應的:

\[a \oplus b = (a \operatorname{or} b) - (a \operatorname{and} b) \]

考慮對於此題擴充套件一下該性質。發現若 \(k\) 進位制下某一位上出現了 \(a_i+b_i\) 的進位,則一定有:

\[c_i = a_i + b_i - k \]

否則有:

\[c_i = a_i + b_i \]

則對於某一位上進位的情況,移項可知 \(k = a_i + b_i - c_i\),否則有 \(a_i + b_i - c_i = 0\)。則可知在 \(k\) 進位制下,\(a+b-c\) 一定可以表示成若干 \(k\) 的冪的和(即發生的進位之和),即有:

\[\begin{aligned} a + b - c &= k^{c_1} + k^{c_2} + \cdots + k^{c_m} &(1\le c_1<c_2 <\cdots)\\ &=k(k^{c_1 - 1} + k^{c_2 - 1} + \cdots + k^{c_m - 1}) \end{aligned}\]

則可知若某個進位制 \(k\) 是對答案有貢獻的,則 \(k\) 一定是 \(a+b-c\) 的因數。有 \(a,b,c\le 10^9\),則可以直接大力列舉 \(a+b-c\) 的所有因數,並大力列舉各位檢查。當 \(a+b-c = 0\) 也即不發生進位時無解。

總時間複雜度 \(O(T\sqrt{v}\log v)\) 級別。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
LL a, b, c, d;
//=============================================================
bool check(LL k_) {
  LL x = a, y = b, z = c;
  while (x || y || z) {
    LL t1 = x % k_, t2 = y % k_, t3 = z % k_;
    if ((t1 + t2) % k_ != t3) return false;
    x /= k_, y /= k_, z /= k_;
  }
  return true;
}
//=============================================================
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 >> a >> b >> c;
    d = a + b - c;
    if (d == 0) {
      std::cout << -1 << "\n";
      continue;
    }
    LL ans = 0;
    if (check(d)) ++ ans;
    for (LL i = 2; i * i <= d; ++ i) {
      if (d % i != 0) continue;
      if (check(i)) ++ ans;
      if (i * i != d && check(d / i)) ++ ans;
    }
    std::cout << ans << "\n";
  }
  return 0;
}

1012

思維。

這題是我倒數第二個過的題哈哈單刷的唐氏簽到失敗是這樣的

這題最後的公式是跟著感覺走半懂不懂地吃了兩發試出來的、、、我現在還不太懂不太好解釋,請參考官方寫得很詳細的題解。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int n, dis[5];
std::string s[4];
//=============================================================
//=============================================================
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;
    for (int i = 1; i <= 3; ++ i) std::cin >> s[i];
    
    for (int i = 1; i <= 4; ++ i) dis[i] = 0;
    for (int i = 0; i < n; ++ i) {
      if (s[1][i] == s[2][i] && 
          s[1][i] == s[3][i] && 
          s[2][i] == s[3][i]) ++ dis[4];
      else if (s[1][i] == s[2][i]) ++ dis[1];
      else if (s[1][i] == s[3][i]) ++ dis[2];
      else if (s[2][i] == s[3][i]) ++ dis[3];
    }
    std::sort(dis + 1, dis + 3 + 1);
    // for (int i = 1; i <= 4; ++ i) std::cout << dis[i] << "--";
    int ans = dis[4] + dis[1] + dis[2] + (dis[3] - dis[2]) / 2;
    std::cout << ans << "\n";
  }
  return 0;
}

1006

最小生成樹,列舉

實際大力題呃呃,賽時被騙了最後半小時才恍然大悟實在唐得一批

考慮能否在直接列舉所有邊做 Kruscal 時,一次性地把所有輪的最小生成樹全做出來。即考慮對於每次列舉到一條邊 \((u, v)\) 時,找到這條邊應當加到哪一輪做的最小生成樹上,以求得這條邊對應答案。

發現若某條邊 \((u, v)\) 應當被加到第 \(i\) 輪上,說明在第 \(1\sim i - 1\) 輪中 \(u, v\) 兩節點均已經聯通,且在 \(i+1\sim \cdots\) 輪中兩節點均不聯通,發現存在單調關係,於是考慮維護每輪中各節點的連通性,並二分檢查各輪中兩節點的連通性即可求得其被加入的輪數。

又發現由於最多隻會做 \(\frac{m}{n-1}\) 輪,且僅有 \(n\) 個節點,完全可以直接開 \(\frac{m}{n-1}\) 個大小為 \(n\) 的並查集來維護連通性,時空複雜度均攤均僅有 \(O(m)\) 級別。

在做最小生成樹的過程中再順便維護下對於每一輪已經加了幾條邊,最後再列舉每條邊,若這條邊對應的輪數上有 \(n-1\) 條邊說明這一輪被做完了即可直接輸出,否則輸出 -1

精細實現並查集總時間複雜度能做到純 \(O(m)\) 級別,懶狗可以像我一樣直接大力上 map,總時間複雜度 \(O(m\log m)\) 級別。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 1e5 + 10;
const int kM = 3e5 + 10;
//=============================================================
int n, m, u[kM], v[kM], ans[kM];
int cnt[kM];
std::map<int, int> fa[kM];
//=============================================================
int find(int id_, int x_) {
  if (!fa[id_].count(x_)) fa[id_][x_] = x_;
  return (x_ == fa[id_][x_]) ? x_ : (fa[id_][x_] = find(id_, fa[id_][x_]));
}
void merge(int id_, int x_, int y_) {
  int fx = find(id_, x_), fy = find(id_, y_);
  fa[id_][fx] = fy;
}
//=============================================================
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;
    for (int i = 1; i <= m; ++ i) ans[i] = -1, cnt[i] = 0, fa[i].clear();
    for (int i = 1; i <= m; ++ i) {
      int u_, v_; std::cin >> u_ >> v_;
      u[i] = u_, v[i] = v_;
      ans[i] = -1;
    }

    int maxround = 0;
    for (int i = 1; i <= m; ++ i) {
      int u_ = u[i], v_ = v[i], nowround = maxround + 1;
      for (int l = 1, r = maxround; l <= r; ) {
        int mid = (l + r) >> 1;
        if (cnt[mid] >= n - 1 || find(mid, u_) == find(mid, v_)) {
          l = mid + 1;
        } else {
          nowround = mid;
          r = mid - 1;
        }
      }
      maxround = std::max(maxround, nowround);
      ans[i] = nowround;
      ++ cnt[nowround];
      merge(nowround, u_, v_);
    }
    
    for (int i = 1; i <= m; ++ i) {
      if (cnt[ans[i]] < n - 1) ans[i] = -1;
      std::cout << ans[i] << " ";
    }
    std::cout << "\n";
  }
  return 0;
}

1005

DP,記憶化搜尋

大概是記憶化搜尋題?

先特判下 \(k\ge 60\)\(n\) 可在區間內任意取,答案即 \(r-l+1\)

然後一個很顯然的想法是直接遞迴求解。記 \(f(l, r, k)\) 表示對區間 \([l, r]\) 進行二分且允許出現 \(k\) 次越界情況下,區間內 \(n\) 的合法取值個數。考慮在這一輪上檢查 \(\operatorname{mid}\) 的結果,則有:

\[f(l, r, k) = 1 + f(l, \operatorname{mid} - 1, k - 1) + f(\operatorname{mid} + 1, r, k) \]

然後這個時候大家寫完過了樣例看著挺優美的就會想直接衝一發交上發現 TLE 了哈哈,然後才發現這東西在 \(k=59\) 時實際上會把整個 \([l,r]\) 全部遍歷一遍,實際是非常醜的東西。

然後又發現實際上 \(f(l, r, k)\) 的答案與區間的具體位置是無關的,實際僅與區間長度 \(\operatorname{len}=r-l+1\) 有關。於是考慮在上述遞迴過程中記憶化 \(g(\operatorname{len},k)\) 表示長度為 \(\operatorname{len}\) 的區間允許 \(k\) 次越界的答案即可。

可以證明在 \(k\) 次二分後二分的區間長度至多僅有 2 種,精細實現時間複雜度能做到 \(O(T\log^2 (r - l + 1))\) 級別,懶狗還可以像我一樣直接大力上 map,總時間複雜度 \(O(T\log^3 (r - l + 1))\) 級別。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pr std::pair
#define mp std::make_pair
//=============================================================
LL l, r, k, ans;
std::map<pr<LL, int>, LL> cnt;
//=============================================================
LL check(LL l_, LL r_, LL c_) {
  if (l_ > r_) return 0;
  if (c_ < 0) return 0;
  if (cnt.count(mp(r_ - l_ + 1, c_))) return cnt[mp(r_ - l_ + 1, c_)];
  
  LL mid = (l_ + r_) / 2ll, delta = 1;
  delta += check(l_, mid - 1, c_ - 1);
  delta += check(mid + 1, r_, c_);
  cnt[mp(r_ - l_ + 1, c_)] = delta;
  return delta;
}
//=============================================================
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 >> l >> r >> k;
    if (k >= 60) {
      std::cout << (r - l + 1) << "\n";
      continue;
    }
    cnt.clear();
    ans = check(l, r, k);
    std::cout << ans << "\n";
  }
  return 0;
}
/*
1
1 100 0
*/

1008

構造。

好玩構造,最人類智慧的一集。

顯然數列 \(a, b\) 均可以構造成 \(1\sim n\) 的排列的形式,若不是排列僅需離散化,即可使其保持性質不變的同時變為排列。

我的做法是考慮把每個節點的 \((a, b)\) 當做座標,以 \(a\) 為橫座標 \(b\) 為縱座標扔到二維座標系上考慮。那麼對於一個大小為 7 的完全二叉樹的樣例可以畫成這個形式:

輸入:

1
7
1 1 2 2 3 3

輸出:

7 7 3 6 6 3 1 5 2 4 4 2 5 1
2024“釘耙程式設計”中國大學生演算法設計超級聯賽(8)

可以發現有如下性質:

  1. 根節點座標一定為 \((n, n)\)
  2. 節點 \(u\) 的子樹中所有節點,一定全部位於該節點座標 \((a_u, b_u)\) 的左下角。
  3. 對於節點 \(u\) 的不同的子節點 \(v', v''\),若有 \(a_{v'} < a_{v''}\),則為了維持性質一定有 \(b_{v'} > b_{v''}\)

有矩陣的包含關係,於是考慮從根開始遞迴構造:

  1. 由於最終答案裡 \(a\) 在前面,則應當首先令編號較小的節點的 \(a\) 儘可能小,於是考慮優先向子樹內編號最小的節點更小的子節點遞迴構造,並嘗試最小化子樹的所有點 \(a\) 的值。
  2. 最優情況下應當構造成節點 \(u\) 滿足:\(a_u\) 為其子節點 \(a\) 最大值+1, \(b_u\)為其子節點 \(b\) 最大值+1。
  3. 由上述性質 3,在最小化子樹內編號最小的節點更小的子節點的 \(a\) 的同時,其子樹內的 \(b\) 應當是更大的,且由構造 2 可知子樹內編號最小的節點最小的子節點 \(v\)\(b_v=b_{u} - 1\)
  4. 可知遞迴構造時,對於某個子樹內節點可以很容易維護其 \(a\)下界 \(A\)\(b\)上界 \(B\)
  5. 對於兩個遍歷順序相鄰的子節點 \(v', v''\),有下界 \(A_{v''} = a_{v'} + 1\),上界 \(B_{v'} = b_{v''} - \operatorname{size}_{v'}\)

由上述性質,發現僅需預處理一下子樹大小 \(\operatorname{size}\) 和子樹內最小編號節點的編號 \(\operatorname{id}\),對每個節點子節點按照 \(\operatorname{id}\) 排個序後遞迴構造。遞迴時下傳每個節點內子樹的 \(a\) 的下界和 \(b\) 的上界,遞迴時直接計算 \(b\),回溯時求得每個節點 \(a\) 的值即可。總時間複雜度 \(O(n)\) 級別。

發現欽定了遞迴構造的順序即 dfs 序。雖然上述構造方法有點麻煩但實際上述構造方法和題解本質是完全一樣的,屬實是殊途同歸了!

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, a[kN], b[kN], sz[kN], id[kN];
std::vector<int> son[kN];
//=============================================================
void dfs1(int u_) {
  sz[u_] = 1;
  id[u_] = u_;
  for (auto v_: son[u_]) {
    dfs1(v_);
    sz[u_] += sz[v_];
    id[u_] = std::min(id[u_], id[v_]);
  }
}
void dfs2(int u_, int a_, int b_) {
  a[u_] = a_, b[u_] = b_;
  -- b_;
  
  for (auto v_: son[u_]) {
    dfs2(v_, a_, b_);
    b_ = b[v_] - sz[v_];
    a_ = a[v_] + 1;
    a[u_] = std::max(a[u_], a[v_] + 1);
  }
}
bool cmp(int fir_, int sec_) {
  return id[fir_] < id[sec_];
}
//=============================================================
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;
    for (int i = 1; i <= n; ++ i) son[i].clear();
    for (int i = 2; i <= n; ++ i) {
      int fa; std::cin >> fa;
      son[fa].push_back(i);
    }
    dfs1(1);
    for (int i = 1; i <= n; ++ i) std::sort(son[i].begin(), son[i].end(), cmp);
    dfs2(1, 1, sz[1]);
    for (int i = 1; i <= n; ++ i) std::cout << a[i] << " " << b[i] << " ";
    std::cout << "\n";
  }
  return 0;
}
/*
1
7 
1 1 2 2 3 3
*/

1003

DP,期望。

1010

01 Trie,資料結構。

我測好牛逼的標記合併!

寫在最後

學到了什麼:

  • 1008:二維偏序關係扔到座標系上考慮。
  • 1010:一種對於位運算的懶標記合併。

昨天是花岡柚子的生日,把這條資訊轉發至三個群電腦就會自動下載柚子社全家桶。我試過了是假的,而且我的電腦自動下載了原神,但昨天真的是花岡柚子的生日。

2024“釘耙程式設計”中國大學生演算法設計超級聯賽(8)

相關文章