比赛题目训练系列06 (2020 ICPC 济南)

比赛题目训练系列06 (2020 ICPC 济南)

训练网址

A. Matrix Equation

  • 给一个 A 矩阵 一个 B 矩阵(矩阵元素为 0 或 1),求有多少个 C 矩阵 满足 A X C = B . C (叉乘 和 点乘)。
  • 其实你把式子展开,就是一道很简单的线性代数题目了啊。不过需要注意的是,对于异或齐次线性方程组,如果初等变换后矩阵的秩为 r,那么解的数量就是 2 n − r 2^{n-r} 2nr。因为每一个自由元都只有两个取值,分别是 0 和 1.
  • 用了别人的图片 2020 icpc济南 A - Matrix Equation (高斯消元求自由元个数)
    比赛题目训练系列06 (2020 ICPC 济南)_第1张图片* 这道题 n = 200,这道题复杂度,用了 bitset 优化,复杂度是 O ( n 4 ) O(n^4) O(n4),但是只用 26ms.(后来想了想,复杂度其实就是 20 0 4 / 32 = 5 e 7 200^4 / 32 = 5e7 2004/32=5e7,26ms 还是能跑完的)。
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const ll mod = 998244353;
ll mod_pow(ll x, ll n) {
	ll res = 1;
	while (n) {
		if (n & 1) res = res * x % mod;
		x = x * x % mod;
		n >>= 1;
	}
	return res;
}
const int maxn = 210;
bitset<maxn> X[maxn], Y[maxn], tmp[maxn];
int N;
int gauss(bitset<maxn> A[]) {
	int r, c;
	for (r = c = 1; c <= N; c++) {
		int t = r;
		for (int i = r; i <= N; i++) {
			if (A[i][c]) {
				t = i;
				break;
			}
		}
		if (A[t][c] == 0) {
			continue;
		}
		swap(A[t], A[r]);
		for (int i = r + 1; i <= N; i++) {
			if (A[i][c]) {
				A[i] ^= A[r];
			}
		}
		r++;
		//printf("### %d\n", r);
	}
	return N - (r - 1);
}
int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) {
			int x;
			scanf("%d", &x);
			if (x) X[i][j] = 1;
		}
	}
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) {
			int x;
			scanf("%d", &x);
			if (x) Y[i][j] = 1;
		}
	}
	ll cnt = 0;

	for (int c = 1; c <= N; c++) {
		for (int r = 1; r <= N; r++) {
			tmp[r] = X[r];
			tmp[r][r] = tmp[r][r] ^ Y[r][c];
		}
		
		cnt += gauss(tmp);
		//printf("*** %lld\n", cnt);
	}
	
	printf("%lld\n", mod_pow(2, cnt));
	return 0;
}

B. Number Game

  • 通过人数太少啦。挖坑

C. Stone Game

  • 题意:有 1 个石子的有 a 堆,2 个石子的有 b 堆,3 个石子的有 c 堆,每次合并任意两堆石子,花费为 (x%3)*(y%3),x y 为石子的个数,问将所有的石子合并成一堆石子之后的花费最小是多少 ?
  • 我们发现和3的倍数合并的话,不会产生花费。我们还发现,两个1合并等价于少两个1多一个2;两个2合并等价于少两个2多一个1. 1和2合并等价于少了一个1一个2,多了一个3.
  • 因此这是一个分类讨论题目。我们想办法把1和2凑起来,1和2石子数量的差模3然后分类讨论即可。
#include
#include
#include
using namespace std;
typedef long long ll;
int main() {
	ll x, y, z;
	cin >> x >> y >> z;
	ll ans;
	if (x > y) {
		ll m = x - y;
		if (m % 3 == 0 || m % 3 == 1) {
			ans = 2 * (y + m / 3) + m / 3;
		}
		else{
			ans = 2 * (y + m / 3) + m / 3 + 1;
		}
	}
	else {
		ll m = y - x;
		if (m % 3 == 0 || m % 3 == 1) {
			ans = 2 * (x + m / 3) + 4 * (m / 3);
		}
		else{
			ans = 2 * (x + m / 3) + 4 + 4 * (m / 3);
		}
	}
	printf("%lld\n", ans);
	return 0;
}

D. Fight against involution

  • 题意:有一门课程 n n n 个学生选,期末要写一篇论文每个同学写的字数有一个下限和一个上限,课程的成绩是按学生字数的排名来给分的,排名越高分数越高,每个同学都想得到更高的成绩,而且他们都想写最少字数,那么在满足每个同学不能比原计划分数低的情况下求出所有同学总共要写的最少字数。
  • 这道题有一个地方很奇怪。正常来讲,碰到 ( 2 , 5 ) , ( 3 , 6 ) (2, 5), (3, 6) (2,5),(3,6) 这种情况,我们应该让他们都写3个字,这样子两个人的分数都最高(毕竟第一个人看到自己可以和第二个人写的字数一样多,就认为能做到和第二个人分数一样高)。但是题目的意思是,第一个人看到自己的上限已经没有第二个人高了,因此自己认为一定不过他,因此选择写2,而不是3,使得自己写的字数最少。What a pity!
  • 将 r 作为第一关键字升序排序, l 作为第二关键字降序排序。选择的话看代码,很好懂。
  • 总之,题意的描述非常垃圾.
#include
#include
#include

#define x first
#define y second

using namespace std;
const int maxn = 100010;
typedef long long ll;
typedef pair<ll, ll> P;

P w[maxn];
int N;
bool cmp(const P& a, const P& b) {
	return a.y < b.y || a.y == b.y && a.x > b.x;
}
int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) {
		scanf("%lld%lld", &w[i].x, &w[i].y);
	}
	sort(w + 1, w + N + 1, cmp);
	ll st = w[1].x, ed = w[1].y;
	ll ans = 0;
	for (int i = 1; i <= N; i++) {
		ans += max(st, w[i].x);
		st = max(st, w[i].x);
	}
	cout << ans << endl;
	return 0;
}

E. Tree Transform

  • 通过人数太少啦。挖坑。

F. Gcd Product

  • 太难了,挖坑

G. Xor Transformation

  • 题意:给了一个X和Y,让每次选择一个A(0<=A
  • 我们很容易看出
    • 如果 (X ^ Y) < X,可以直接输出这个答案
    • 否则,X 的二进制位数一定大于 Y(可用反证法证明),然后把X 和 Y 共同的位数的部分,每一位数字两两异或。把 X 比 Y 大的二进制部分,输出。这样就可以了。
    • 有一个地方考试错了,这次又想错了。就是后面那一个步骤。并不是把 X 的最高位的1去掉,而是把 X 比 Y 高的所有位的1都要去掉。因此第二个数输出的不是 1 < < ( X . s i z e ( ) − 1 ) 1 << (X.size() - 1) 1<<(X.size()1),而是那一段数字的十进制形式。
#include
#include
#include
#include
using namespace std;
typedef long long ll;
ll X, Y;
vector<int> ten_two(ll x) {
	vector<int> v;
	while (x) {
		v.push_back(x % 2);
		x >>= 1;
	}
	return v;
}
ll two_ten(vector<int> v) {
	ll res = 0;
	for (int i = 0; i < v.size(); i++) {
		res += (ll)v[i] * (1LL << i);
	}
	return res;
}
int main() {
	cin >> X >> Y;
	if ((X ^ Y) < X) {
		printf("1\n%lld\n", X ^ Y);
		return 0;
	}
	printf("2\n");
	vector<int> xx = ten_two(X), yy = ten_two(Y);
	int szx = xx.size(), szy = yy.size();
	vector<int> ans1;
	for (int i = 0; i < szy; i++) ans1.push_back(xx[i] ^ yy[i]);
	printf("%lld ", two_ten(ans1));
	vector<int> ans2;
	ll res = 0;
	for (int i = szy; i < szx; i++) {
		res += (ll)xx[i] * (1LL << i);
	}
	printf("%lld\n", res);
	return 0;
}

H. Path Killer

  • 太难了,挖坑

I. Random Walk On Tree

  • 通过人数太少啦。挖坑。

J. Tree Constructer

  • 给定一棵 n n n 个结点的树,现要你将每个结点 u u u 赋上一个权值 a u a_u au,使得若 x , y x, y x,y 之间有边,则 a x ∣ b y = 2 60 − 1 a_x | b_y= 2^{60} − 1 axby=2601 x , y x,y x,y之间没有边,则 a x   o r   a y ≠ 2 60 − 1 a_x \ or \ a_y \ne 2^{60}-1 ax or ay=2601,输出 a 1... n a_{1...n} a1...n

问题分成两种情况,有边和无边,混在一起考虑比较困难,想到分开来考虑,可以想到二分图的模型。树本身就是一个二分图(没有奇环)。
将树上的点黑白染色后分成两个部分。先来考虑两个部分内部的情况,可以将白点的最末两位赋上01,将黑点的最末两位赋上10,这样白点内部和黑点内部就能满足限制了,且白点和黑点的最后两位或起来为11,而边只能是一端白点,一端黑点,所以也满足限制。再来考虑黑白之间的情况,可以从黑部和白部中点数较少的那个部分着手,不妨设黑部点数较少,我们将第一个黑点的高58位赋上111…1110,第二个赋上111…1101,第三个赋上111…1011,依此类推,因为黑部点数较少,所以少于等于50个,所以58位足以标识所有的点。然后对于每个白点,它连了几个黑点,就把那几个黑点高58位中为0的位置赋上1,其余位置赋上0,即可满足限制。
————————————————
原文链接:https://blog.csdn.net/weixin_43466755/article/details/112383754

  • 中间有几个地方指的一说。
    • 判断二分图的黑点和白点,可以用结点离树根的距离的奇偶性。因为每一层的结点之间一定没有边。因此可以把一层放左边,一层放右边。
    • int flag = (cnt_odd < cnt_even);,然后 if ((d[i] & 1) == flag)就可以判断在奇数多的情况下操作还是在偶数多的情况下操作。
    • 对于每个白点,它连了几个黑点,就把那几个黑点高58位中为0的位置赋上1,其余位置赋上0。这个地方不要用ans[u] |= (~ans[v]),因为非运算会把整个64位都反过来,相当于反码。应该ans[u] |= (B ^ ans[v]),这样子就把0给抠出来了。
#include
#include
#include
using namespace std;
typedef long long ll;
const int maxn = 110, maxm = 210;
int h[maxn], e[maxm], ne[maxm], idx;
int d[maxn], N, CNT[maxn];
ll ans[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int fa) {
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (v == fa) continue;
		d[v] = d[u] + 1;
		dfs(v, u);
	}
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d", &N);
	for (int i = 1; i < N; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
	}
	dfs(1, -1);
	int cnt_odd = 0, cnt_even = 0;
	for (int i = 1; i <= N; i++) {
		if (d[i] & 1) {
			cnt_odd++;
		}
		else {
			cnt_even++;
		}
	}
	
	int flag = (cnt_odd < cnt_even);
	int cnt = 0;
	ll B = (1LL << 58) - 1; 
	
	for (int i = 1; i <= N; i++) {
		//可以用这个方式判断 cnt_odd 多还是 cnt_even 多。
		if ((d[i] & 1) == flag) {
			ans[i] |= B;
			ans[i] ^= (1LL << cnt);
			cnt++;
		}
	}
	for (int u = 1; u <= N; u++) {
		if ((d[u] & 1) != flag) {
			for (int i = h[u]; i != -1; i = ne[i]) {
				int v = e[i];
				ans[u] |= (B ^ ans[v]);
				//printf("%lld\n", ans[u]);
			}
		}
	}
	for (int i = 1; i <= N; i++) {
		ans[i] <<= 2;
		if (d[i] & 1) ans[i] |= 1;
		else ans[i] |= 2;
	}
	for (int i = 1; i <= N; i++) {
		printf("%lld%c", ans[i], i == N ? '\n' : ' ');
	}
	return 0;
}

K. Kth Query

  • 今天算法设计与分析课刚讲了用快速排序求第 k 个小数。下面来一道变形题目
    在这里插入图片描述
  • 讲解很详细
  • 代码很好
  • 剩下补充的解释都在批注里面了。
#include
#include
#include
#include
using namespace std;
const int maxn = 100010, maxm = 30 * maxn;
int w[maxn], sz[maxm], son[maxm][2], idx;
int N, M;
const int lg = 29, INF = (1 << 30);
vector<vector<int>> kth;
void insert(int x) {
	int p = 0;
	for (int i = lg; i >= 0; i--) {
		sz[p]++;
		int u = ((x >> i) & 1);
		if (!son[p][u]) son[p][u] = ++idx;
		p = son[p][u];
	}
	sz[p]++;
}
void dfs(int u, int d) {
	int l = son[u][0], r = son[u][1];
	if (!l && !r) {
		kth[u][1] = 0;
		return;
	}
	if (l) dfs(l, d - 1);
	if (r) dfs(r, d - 1);
	for (int i = 1; i <= sz[u]; i++) {
		kth[u][i] = INF;
		//如果当前的 S 的第 d 位是 0。
		if (sz[l] >= i) kth[u][i] = min(kth[u][i], kth[l][i]);
		//那么答案就在右子树里面
		else kth[u][i] = min(kth[u][i], (1 << d) ^ kth[r][i - sz[l]]);
		//如果当前的 S 的第 d 位是 1.
		if (sz[r] >= i) kth[u][i] = min(kth[u][i], kth[r][i]);
		else kth[u][i] = min(kth[u][i], (1 << d) ^ kth[l][i - sz[r]]);
	}
}
void build() {
	//用resize,防止爆空间。
	for (int i = 1; i <= N; i++) {
		insert(w[i]);
	}
	kth.resize(idx + 1);
	for (int i = 0; i <= idx; i++) {
		kth[i].resize(sz[i] + 1);
	}
	for (int i = 0; i <= N; i++) {
		kth[0][i] = INF;
	}
	dfs(0, lg);
}
int walk(int& p, int& k, int t) {
	if (son[p][t] && sz[son[p][t]] >= k) {
		p = son[p][t];
		return 0;
	}
	else {
		if (son[p][t]) k -= sz[son[p][t]];
		p = son[p][1 ^ t];
		return 1;
	}
}
int query(int x, int lim, int k, int flag) {
	int p = 0, S = 0, res = INF;
    for(int i = lg; i >= 0; i--){
        int t = ((x >> i) & 1);
        //L的第lim的位置一定是0,R第lim的位置一定是1.
        //我们让第lim的位置仍然贴合下界(上界)
        //因此,从第 lim - 1 的位置,就可以0和1随便填了。
        if(i < lim && t == flag){
            //t==flag,比如L这一位是0的时候,那么S的这一位可以填1也可以填0
            //当L这一位是1的时候,S的这一位只能填1. 因为要贴合下界。对于R也是同理
            int tp = p, ts = S, tk = k;
            //这一步表示不贴和下界的情况,由于后面的数可以随意填,res 就是当前的S,后面拼上 t^1,然后再拼上 kth[p的儿子][k].
            if(walk(tp, tk, t ^ 1)) ts ^= (1 << i);
            res = min(res, ts ^ kth[tp][tk]);
        }
        //这一步表示贴合下界(上界)的情况。
        if(walk(p, k, t)) S ^= (1 << i);
    }
    //S贴合下界的情况要在最后一步更写一下答案。
    res = min(res, S ^ kth[p][k]);
    return res;
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) scanf("%d", &w[i]);
	build();
	while (M--) {
		int L, R, K;
		scanf("%d%d%d", &L, &R, &K);
		int p = lg;
		while (p >= 0 && ((L >> p) & 1) == ((R >> p) & 1)) p--;
		printf("%d\n", min(query(L, p, K, 0), query(R, p, K, 1)));
	}
	return 0;
}

L. Bit Sequence

  • 又想明白了一点点数位 d p dp dp。我们知道,对于 n n n 个数位,每个位置可以填 0 或 1 的话,有 2 n 2^n 2n 种结果。但是,我们状态的每一种表示,比如 d p ( p o s , l i m , x ) dp(pos, lim, x) dp(pos,lim,x) 表示在 limit的状态下,在pos的位置上填x,在0~pos-1这些位置随便填,有多少种状态。
  • 题意:给出一个长度为 m ( m < = 100 ) m(m<=100) m(m<=100) 的01数组a,求 [ 0 , L ] [0,L] [0,L] 中有多少个 x x x 满足: x + i ( i ∈ [ 0 , m − 1 ] ) x+i(i\in[0,m-1]) x+i(i[0,m1]) 二进制中的1的个数的奇偶性为 a [ i ] a[i] a[i].
  • 我们可以这样分析:
    • 首先,从低位开始数的第7位到更高位,我们采用数位dp的方式维护他们的两个信息,一个是的1的个数,一个是从第8位开始到更高位连续的1的个数。这样子,1~7位我们可以暴力枚举,看看把这128种(lim=1的时候可能少于128种)数字直接挂在后面。然后把这个数字从0加到m-1,看看当前的这个数字是否满足 a 数组。这样子就可以查出来个数了。
    • 不过有几个细节. 最后在从 0 加到 m-1 的时候,可能会产生进位。进位是否改变1的个数的奇偶性,取决于从第8位开始往前连续的1的个数。相当于把这些1拿下来,在最前面放了个1. 我们假设前若干位的1的个数的奇偶性位s,连续的1的个数的奇偶性为t。如果不进位的话,我们只需要检查 (i + j) ^ s == a[j];进位的话,我们查看 s ^ t ^ (i + j). s ^ t 相当于把最后连续的若干个1拿下来,然后把这个 (i + j) 加上去。
    • 这道题由于需要两个状态(s, t)才能确定前面若干位,因此需要开在 pos 与 lim 之外再开两维。
#include
#include
#include
#include
using namespace std;
typedef long long ll;
ll dp[64][2][2][2];
const int maxn = 110;
bitset<maxn> w, L_b;
int M;
ll L;
int is_even(int x) {
	int res = 0;
	while (x) {
		res ^= (x & 1);
		x >>= 1;
	}
	return res;
}
ll calc(int lim, int s, int t) {
	int end = lim ? L % 128 : 127;
	//printf("*** %d\n", end);
	int res = 0;
	for (int i = 0; i <= end; i++) {
		int f = 1;
		for (int j = 0; j < M; j++) {
			if (i + j < 128) f &= ((is_even(i + j) ^ s) == w[j]);
			else f &= ((is_even(i + j) ^ s ^ t) == w[j]);
		}
		res += f;
	}
	return res;
}
ll dfs(int pos, int lim, int s, int t) {
	//s表示第8位及之前的数字的1的个数,t表示从第8位开始往前数连续的1的个数。
	if (dp[pos][lim][s][t] != -1) return dp[pos][lim][s][t];
	if (pos <= 6) {
		return dp[pos][lim][s][t] = calc(lim, s, t);
	}
	int up = lim ? L_b[pos] : 1;
	ll res = 0;
	for (int i = 0; i <= up; i++) {
		//i & (!t) 很有趣。t = 0 表示最后连续的1是偶数。
		//如果当前位填 0 的话,那么连续被打断,最后连续的1的个数显然是偶数(连续个数为0).
		//如果当前填1的话,那么就把之前的状态 t 反转一下即可。
		res += dfs(pos - 1, i == up && lim, s ^ i, i & (!t));
	}
	//注意贴合上界的情况下可能被复用。
	if (lim == 0) dp[pos][lim][s][t] = res;
	return res;
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		w.reset();
		memset(dp, -1, sizeof dp);
		scanf("%d%lld", &M, &L);
		for (int i = 0; i < M; i++) {
			int x;
			scanf("%d", &x);
			w[i] = x;
		}
		int len = 0;
		//L后面还要用,别在这里清零了
		ll _L = L;
		while (_L) {
			L_b[len++] = (_L & 1);
			_L >>= 1;
		}

		printf("%lld\n", dfs(len - 1, 1, 0, 0));
	}
	return 0;
}

M. Cook Pancakes!

  • 有 N 张饼需要摊,两面都需要摊。一个锅里最多可以放K张饼,问最少需要多少时间。
  • 只有一个地方需要注意,就是小心 2 * N <= K 的情况
#include
#include
#include
using namespace std;
int main() {
    int N, K;
    scanf("%d%d", &N, &K);
    if (N < K) printf("2\n");
    else printf("%d\n", (2 * N + K - 1) / K);
    return 0;
}

你可能感兴趣的:(ACM题目整理)