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(); }
posted @ 2021-11-28 21:50  kpole  阅读(1770)  评论(1编辑  收藏  举报