2021 上海 ICPC 區域賽


rank鏈接

簽到題:EDGI

銅題:H

銀題:JKM

金牌:B

D Strange Fractions

\(x = {a \over b}\),那么有 \({p\over q} = x + {1 \over x}\) ,可以轉換為求解 \(qx^2-px+q = 0\) 的正整數根。

使用求根公式,判斷 \(p^2-4*q^2\) 是否是完全平方數即可。然后取 \({a\over b}={p+d\over 2q}\) 。復雜度 \(O(T\log q)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T;
ll p, q;
int main(){
  scanf("%d", &T);
  while(T--){
    scanf("%lld%lld", &p, &q);
    ll d = p * p - 4 * q;
    if(d < 0) {
      puts("0 0");
    } else {
      ll t = sqrt(d);
      if(t * t == d) printf("%lld %lld\n", p + t, 2 * q);
      else puts("0 0");
    }
  }
}

E Strange_Integers

整體排序后,貪心的去選即可。證明:設 \(f[i]\) 表示前 \(i\) 個能夠選出的最多的個數,那么 \(f[i]\) 隨着 \(i\) 遞增,求解 \(f[j]\) 時(\(j\gt i\)),去找盡量大的 \(i\) 滿足 \(a[j]-a[i] \ge k\)

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, k, a[N];

int main(){
    cin >> n >>k;
    for(int i=1;i<=n;i++){
        scanf("%d", &a[i]);
    }
    sort(a + 1, a + 1 + n);
    int res = 1;
    int last = a[1];
    for(int i=2;i<=n;i++){
        if(a[i] - last >= k) {
            last = a[i];
            res ++;
        }
    }
    cout <<res <<endl;
    return 0;
}

G Edge Groups

\(n\) (奇數)個點的樹,\(n-1\) 條邊分成 \({n-1\over 2}\) 組,每組兩條邊並且這兩條邊要有一個公共點,詢問分組的方案數。

考慮樹形DP,子樹 \(x\) 中若有偶數個點,那么奇數條邊必然無法分組,需要 \(x\) 連向其父親的邊。如果有奇數個點,那么偶數條邊可以分組。

\(d[x]\) 為分組 \(x\) 子樹中的邊的方案數,設 \(x\) 的孩子 \(y\) 中,有 \(a\)\(y\) 需要 \((x,y)\) 這條邊與 \(y\) 子樹中的邊配對,有 \(b\)\(y\)\((x,y)\)\(x\) 這里配對的。那么當 \(b\) 是奇數時還需要 \(x\) 與其父親連接的邊。

也就是說,\(x\)\(a\) 個子樹點數是偶數,\(b\) 個子樹點數是奇數。現在只需要考慮把 \(b\) 個子樹兩兩分組即可,如果 \(b\) 是奇數,那么還需要 \((x,fa)\) 加入其中。

\(n\) 個元素,每組兩個分成 \({n\over 2}\) 組的方案數是 \(f[n]\),有遞推式 \(f[n] = f[n-2] * (n-1)\)

所以:

\[d[x] = \begin{cases} f[b]*\prod d[y] &\text{b is even} \\ f[b+1] * \prod d[y] &\text {b is odd} \end{cases} \]

每個子樹的方案數依舊獨立,所以累計貢獻時還是用乘法,只是其中 \(b\) 個是有順序的,這個順序個數就是 \(f\)

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 100010, mod = 998244353;
int n, sz[N];
vector<int> g[N];
ll d[N], f[N];
void dfs(int x, int fa){
  sz[x] = 1; d[x] = 1;
  int cnt = 0;
  for(auto &y : g[x]) {
    if(y == fa) continue;
    dfs(y, x);
    sz[x] += sz[y];
    d[x] = d[x] * d[y] % mod;
    if(sz[y] & 1) cnt ++;
  }
  if(cnt & 1) cnt ++;
  d[x] = d[x] * f[cnt] % mod;
}
int main(){
  scanf("%d", &n);
  for(int i=1;i<n;i++){
    int x, y; scanf("%d%d", &x, &y);
    g[x].push_back(y); g[y].push_back(x);
  }
  f[0] = 1;
  for(int i=2;i<=n;i+=2){
    f[i] = f[i-2] * (i-1) % mod;
  }
  dfs(1, 0);
  printf("%lld\n", d[1]);
  return 0;
}

H Life is a Game

\(n\) 個點 \(m\) 條邊,有點權和邊權,\(q\) 個詢問,每個詢問 \(x,k\) 表示從 \(x\) 出發,帶着 \(k\) 的能力,通過一條邊的條件是能力大於這條邊的權值,當第一次到達一個點時可以將該點的權值累加到能力上,求可以獲得的最大能力。


考慮離線詢問,每個點帶有若干個詢問,每個詢問包含初試能力值和詢問ID。然后從小到大枚舉每條邊\((x,y,w)\),設 \(v[x]\)\(x\) 的點權,那么對於從 \(x\) 出發的詢問,如果能力值 \(k+v[x] < w\),那么說明它無法到達除去 \(x\) 之外的所有點,它的答案就是 \(k+v[x]\),緊接着把該詢問從 \(x\) 的詢問集合中刪除。之后對於 \(y\) 做同樣的處理。

此時,\(x\)\(y\) 中詢問都可以通過 \((x,y,w)\) 這條邊,所以,可以將 \(x\)\(y\) 合並成一個集合,然后該集合的點權和就是 \(v[x]+v[y]\)。這里可以用一個帶權並查集合並。另外詢問也需要合並,按照啟發式合並的思想,每次小的集合向大的集合合並即可。

總體復雜度為 \(O(m\log m+q\log ^2 q)\)

關於啟發式合並的思想簡單提一下,每次小的集合向大的集合合並,那么對於小的集合而言,大小擴大二倍以上,所以每個點從小的集合刪除再加入大的集合的次數不會超過 \(\log_2^n\),整體復雜度就是 \(n\log_2^n\)。本題中因為需要對詢問按照能力值排序,所以需要用 multiset 維護,所以是兩個 log 的復雜度即 \(q\log^2 q\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
int n, m, q;
struct Edge
{
    int x, y, w;
    bool operator<(const Edge &b) const
    {
        return w < b.w;
    }
} e[N];
int f[N], v[N], rs[N];
multiset<pair<int, int>> st[N];
int get(int x) { return x == f[x] ? f[x] : f[x] = get(f[x]); }
int main()
{
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
        scanf("%d", &v[i]), f[i] = i;
    for (int i = 1; i <= m; i++)
    {
        int x, y;
        scanf("%d%d%d", &e[i].x, &e[i].y, &e[i].w);
    }
    sort(e + 1, e + 1 + m);
    for (int i = 1; i <= q; i++)
    {
        int x, k;
        scanf("%d%d", &x, &k);
        st[x].insert({k, i});
    }
    int p = 1;
    for (int i = 1; i <= m; i++)
    {
        int x = e[i].x, y = e[i].y, w = e[i].w;
        x = get(x);
        y = get(y);
        if (x == y)
            continue;
      	// 從 x 出發的集合無法跨過 w
        while (st[x].size() && (*st[x].begin()).first < w - v[x])
        {
            int id = st[x].begin()->second;
            rs[id] = v[x] + st[x].begin()->first;
            st[x].erase(st[x].begin());
        }
        while (st[y].size() && (*st[y].begin()).first < w - v[y])
        {
            int id = st[y].begin()->second;
            rs[id] = v[y] + st[y].begin()->first;
            st[y].erase(st[y].begin());
        }
        // 啟發式合並,從小的集合合並到大的集合中
        if (st[x].size() > st[y].size())
            swap(x, y);
        while (st[x].size())
        {
            st[y].insert(*st[x].begin());
            st[x].erase(st[x].begin());
        }
        f[x] = y;
        v[y] += v[x];
    }
	// 還有一些詢問留在最后處理
    for (int i = 1; i <= n; i++)
    {
        int x = get(i);
        for (auto &t : st[x])
        {
            rs[t.second] = v[x] + t.first;
        }
        st[x].clear();
    }
    for (int i = 1; i <= q; i++)
    {
        printf("%d\n", rs[i]);
    }
}

I Steadily Growing Steam

\(n\) 個卡牌,有價值 \(v_i\) 和點數 \(t_i\) ,你需要從中選出兩組卡牌使得其點數和相等,然后最大化這兩組的價值和。另外你可以選擇不超過 \(k\) 個不同的卡牌,使得其 \(t_i\) 變成 \(2t_i\)

\(k \le n \le 100, t_i\le 13, |v_i|\le 10^9\)


考慮背包,我們僅關心這兩組的點數和之差,所以可以把這個當做背包體積,如果第 \(i\) 個卡牌放入第一個集合,體積增加 \(t_i\),否則體積減少 \(t_i\),最后只需要取體積為 0 時候的答案即可。

另外還有一個體積是使卡牌點數翻倍的次數,所以狀態可以定義為 \(d[i][j][w]\),表示前 \(i\) 張卡牌,翻轉恰好 \(j\) 次,兩個集合體積差為 \(w\) 時的最大價值和。那么轉移方程有:

\[d[i][j][w] = max \begin{cases} d[i-1][j][w] & \text{不選 i}\\ d[i-1][j][w+t_i] + v_i & \text{把 i 放入第一個集合}\\ d[i-1][j][w-t_i] + v_i & \text{把 i 放入第二個集合}\\ d[i-1][j-1][w+2t_i] + v_i & \text{把 i 點數翻倍,放入第一個集合}\\ d[i-1][j-1][w-2t_i] + v_i & \text{把 i 點數翻倍,放入第二個集合}\\ \end{cases} \]

此時,時間復雜度 \(O(n^3t_{max})\),其中 \(t_{max} = 26\) 。注意到空間復雜度也是 \(O(n^3t_{max})\),顯然需要滾動數組優化。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 105, M = 2605;
int n, k, v[N], t[N], T = 1300;
ll d[2][N][M];
/*
t_max = 26, 總和是 2600,但兩個集合的差最多只需要維護 [-1300,1300] 即可,因為差距更大的話最終不會回到 0
數組訪問下標不能為負數,所以加一個偏移量 T 即可。
*/
int main(){
  scanf("%d%d", &n, &k);
  rep(i,1,n) scanf("%d%d", &v[i], &t[i]);
  memset(d, 0xcf, sizeof d); // v[i] 范圍是 [-1e9,1e9],所以答案提前初始化為負無窮
  d[0][0][T] = 0; // 一開始什么都不選,答案為 0
  int z = 1; // 滾動數組的下標
  for(int i=1;i<=n;i++){
    memset(d[z], 0xcf, sizeof d[z]);
    for(int j=0;j<=k;j++){
      for(int w=-1300;w<=1300;w++){
        for(int p=-2;p<=2;p++){ // 5 種物品,體積分別為 -2t[i],-t[i],0,t[i],2t[i],對應的價值為 v[i],v[i],0,v[i],v[i]
          int wt = w + p * t[i]; // 轉移后的體積
          if(wt < -1300 || wt > 1300) continue; // 超出范圍就過濾掉
          if(j > 0) { // j > 0 表示 5 種物品都可以考慮轉移
            // 轉移代碼簡略了一些,第一個點是 abs(p) = 2 時,要從 j - 1轉移;第二個點是 p != 0 時價值為 v[i]
            d[z][j][wt+T] = max(d[z][j][wt+T], d[z^1][j - (abs(p) == 2)][w + T] + (p == 0 ? 0 : v[i]));
          } else if(abs(p) <= 1) { // j = 0 只能考慮不加倍點數的轉移
            d[z][j][wt+T] = max(d[z][j][wt+T], d[z^1][j - (abs(p) == 2)][w + T] + (p == 0 ? 0 : v[i]));
          }
        }
      }
    }
    z^=1;
  }
  ll res = 0;
  // 狀態定義為恰好翻倍 k 次的最大價值,所以要遍歷所有的 [1,k]。
  for(int i=0;i<=k;i++) res = max(res, d[n&1][i][T]); 
  printf("%lld\n", res);
}

J Two Binary Strings Problem

給出兩個長度為 \(n(n\le 50000)\) 的 01 串 \(A,B\),對於每一個 \(1\le k\le n\),遍歷對於所有的 \(1\le i \le n\),定義 $C_i $ 為集合 \(\{A_{max(i-k+1,1)},...,A_{i-1},A_i\}\) 里面的眾數(題目定義:如果 1 的個數嚴格多於一半,就是 1,否則是 0),如果每個 \(C_i = B_i\) 成立,那么就輸出 1,否則輸出 0。


題意很繞,舉個例子:

image-20211128212749734

\(A = 0110011\),對於每個 \(K\),可以將其轉換成每一行上的 \(C\),如果第 \(i\) 行上的 \(C=B\),那么就輸出 1,否則輸出 0。

這題需要找規律遞推,不妨借助這個圖,去發現一些規律。

考慮對於每個 \(A_i\),一次性求出 \(k\in [1,n]\) 時的所有 \(C_i\)。可以用 bitset 快速維護,並且利用之前的答案遞推。

當前枚舉 \(i\),試圖找到一個最近的 \(j\),滿足 \(A_{j+1},\cdots,A_i\) 中 0 和 1 的個數相同。考慮如果借助計算出的 \(n\)\(C_j\) 去遞推 \(n\)\(C_i\)

如果 \(A_i=1\),那么當 \(k = 1,2,\cdots,i-j-1\) 時,\(C_i=1\)。因為這一段數字中,1的個數永遠比 0 的個數多;當 \(k=i-j\) 時,\(C_i=0\),因為此時 0 和 1 的個數恰好一樣多。那么當 \(k=t>i-j\) 時,就可以使用 \(C_j\) 去遞推了。原因在於每個位置,1 和 0 的大小關系是等同的。

遞推例子:

image-20211128212939598

細節上如何遞推:第 \(i\) 列的 \(n\) 個數字可以看做一個 \(n\) 位二進制(第 \(p\) 位對應 \(k=p+1\) 時的 \(C_i\)),使用 bitset 維護,叫做 \(f_i\)

找到上面描述的那個 \(j\),如果找不到:

  • 如果 \(A_i=1\),那么 \(f_i\) 的所有 \(n\) 位都是 1
  • 如果 \(A_i=1\),那么 \(f_i\) 的所有 \(n\) 位都是 0

如果找到了 \(j\)

  • 如果 \(A_i = 1\),那么 \(f_i\)\(0\sim i-j-2\) 位都是 1(對應 \(k=1,\cdots,i-j-1\)),第 \(i-j-1\) 位是 0。然后 \(f_i = f_i | (f_{j}\ll (i-j))\)
  • 如果 \(A_i=0\),那么 \(f_i\)\(0\sim i-j-1\) 位都是 0,然后 \(f_i = f_i|(f_j\ll (i-j))\)

最后要計算所有合法的 \(k\),用一個長度為 \(n\) 的二進制數 \(st\) 表示合法狀態,起初所有位上都是 1,遍歷所有 \(i\),只需要每次與\(f_i\) 中的合法狀態做位與運算即可。當 \(B_i=1\) 時,\(f_i\) 中為 1 的位合法,否則 \(f_i\) 中為 0 的位合法。

其他詳細情況可見代碼。細節蠻多,可以考慮下標從 0 開始,會省去二進制數字中第 0 位的特殊處理。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;
const int N = 50010;
int T, n;
char a[N], b[N];
bitset<N> c[N], cn, rs;

void solve(){
  unordered_map<int,int> mp; mp[0] = -1;
  int dis = 0;
  // 先制作一個 0~n-1 位都是 1 的二進制數字
  cn.reset(); rs.reset();
  for(int i=0;i<n;i++) cn[i] = 1;
  rs = cn; // 保存答案,也就是合法狀態 st
  for(int i=0;i<n;i++){
    dis += (a[i] == '1' ? 1 : -1);
    c[i].reset(); // c[i] 全部置為 0
    if(mp.count(dis)) {
      int j = mp[dis];
      if(a[i] == '1') { // c[i] 的 0 ~ i-j-2 位都是1
        c[i] |= cn >> (n - (i - j - 1));
      }
      if(j >= 0) // 如果 j = -1,不需要或運算
        c[i] |= c[j] << (i - j);
    } else { // 不存在 j
      if(a[i] == '1') c[i] = cn;
    }
    if(c[i][i] == 1) { // 如果看不懂,思考上面的 C_3 的后綴 1
      c[i] |= (cn >> i) << i;
    }
    mp[dis] = i;
    if(b[i] == '1') {
      rs &= ~(c[i] ^ cn);
    } else {
      rs &= ~c[i];
    }
  }
  for(int i=0;i<n;i++) {
    if(rs[i] == 1) putchar('1');
    else putchar('0');
  }
  puts("");
}
int main(){
  scanf("%d", &T);
  while(T--){
    scanf("%d", &n);
    scanf("%s%s", a, b);
    solve();
  }
}

K Circle of Life

構造一個長度為 \(n\) 的 01 串,每一次變換的規則如下:

  1. \(i\) 位如果為 1,那么變換后第 \(i+1\) 和第 \(i-1\) 位為 1。最左邊和最右邊的溢出不需要管。
  2. 如果\(i\)\(i+2\) 位都為 1,那么這兩個 1 會在 \(i+1\) 發生沖撞抵消,第 \(i+1\) 位變換后為 0。
  3. 如果 \(i\)\(i+1\) 位都為 1,那么變換后 \(i\)\(i+1\) 位也為 0。

要求變換前與變換后,串內必須有 1,並且你需要保證構造出的串,可以在 \(2n\) 次變換內,出現兩個完全相同的串。


打表找規律即可。附打表代碼:

#include<bits/stdc++.h>
using namespace std;
#define rep(i,j,k) for(int i=int(j);i<=int(k);i++)
#define per(i,j,k) for(int i=int(j);i>=int(k);i--)
typedef long long ll;

bool check(string s){
	for(auto &c : s) if(c == '1') return true;
	return false;
}

bool ok(string s) {
	unordered_map<string,int> mp;
	int cnt = 0;
	while(check(s)) { // 每次變換需要保證二進制串中有 1
		mp[s] = 1;
		string t = s;
		for(int i=0;i<s.length();i++){ // 遍歷 s 的每一位
			t[i] = '0';
			int flag = i > 0 ? s[i-1] == '1' : false; // 查看 s[i-1] 是否為 1
			int flag2 = i + 1 < s.length() ? s[i + 1] == '1' : false; // 查看 s[i+1] 是否為 1
			int flag3 = s[i] == '1'; // 查看 s[i] 是否為 1
            // t[i] 為 1 當且僅當 (s[i-1] 或 s[i+1] 其中有一個是 1 並且 s[i] 為 0)
			if(flag + flag2 + flag3 == 1 && flag3 == 0) t[i] = '1'; 
		}
		if(mp[t]) return true; // 如果出現相同,則合法
		s = t;
		cnt ++;
		if(cnt > 2 * s.length()) return false; // 如果變換次數超過上限,返回false
	}
	return false;
}

void solve(int n) {
	cout << "group " << n << " : "<< endl;
	for(int i=0;i<(1<<n);i++){ // 二進制枚舉
		string s = "";
		for(int j=0;j<n;j++){
			if(i >> j & 1) s += "1";
			else s += "0";
		}
		if(ok(s)) {
			cout << s << endl;
			// break;
		}
	}
}

int main(){
  int n; // 輸入 n,找到長度為 n 的所有合法串
  cin >> n;
  solve(n);
}

打出表來之后,可以先觀察長度為 2、3、4、5、6、7、8 等長度的串。發現可以使用 1001 作為一個單元去構造后面延長的循環串。

AC代碼:

#include <bits/stdc++.h>
using namespace std;
#define rep(i, j, k) for (int i = int(j); i <= int(k); i++)
#define per(i, j, k) for (int i = int(j); i >= int(k); i--)
typedef long long ll;
int n;
string s[] = {"0", "0", "01", "", "1001", "10001", "011001", "0101001"};
void solve() {
  cin >> n;
  if (n == 3) {
    puts("Unlucky");
  } else {
    if (n <= 7) {
      cout << s[n] << endl;
    } else {
      int cnt = 0;
      if (n % 4 == 0) {
        cnt = n / 4;
      } else if (n % 4 == 1) {
        cout << s[5];
        cnt = n / 4 - 1;
      } else if (n % 4 == 2) {
        cout << s[2];
        cnt = n / 4;
      } else {
        cout << s[7];
        cnt = n / 4 - 1;
      }
      for (int i = 0; i < cnt; i++)
        cout << "1001";
      cout << endl;
    }
  }
}
int main() { solve(); }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM