Week Round 30

T1 是无意义题,就不说了。这次周赛出得最差的题目就是 T1。

T2: ABC282E

题目描述

n 个数 ai,你每次可以选出两个数 aiaj,获得 (aiaj+ajai)modM 分,并选择这两个数中的一个数删掉,求最大得分。

1n500

题目思路

我们把选出的两个数看成一条边,(aiaj+ajai)modM 就是边权。先对两两个数建边,可以得到一个图。可以知道,通过操作得到的一个图不存在环。即选出的子图是一颗树。

也就是说:对原图求最大生成树,就是答案。

T3: CF1954E

题目描述

给定一个长度为 n 的序列 a,你还有一个属性 k,定义一次操作为:

  • 选择 a 中一段极长的区间 [l,r],满足 mini=lrai>0

    在这里,极长的区间定义为 [l,r] 满足条件但 [l1,r][l,r+1] 不满足条件。

  • i[l,r]N,执行 aiaik

定义 f(k0) 为当 k=k0 时,为使 maxi=1nai0 的最小操作数。

你需要分别求出 f(1),f(2),f(3),,f(maxi=1nai) 的值。

保证 1n1051ai105

题目思路

这次周赛出得最好的题目之一就是 T3。

为了简单起见,我们从 k=1 开始。

第一道闪电可以向任何怪物发射,因为它总是会扩散到所有怪物身上。我们将继续发射闪电,直到有怪物死亡。当一个或多个怪物死亡时,问题就会分解成几个独立的子问题,因为没有闪电会穿过死亡的怪物。而且我们不管选择什么怪物来发射闪电,分成的子问题都是相同的。这意味着不存在 "最少秒数 "的概念——答案并不取决于发射闪电的怪物的选择。这个结论真的是妙!

那么我们该如何计算这个答案呢?攻击第一个怪物,直到它死亡。这需要 a1 秒。然后我们继续攻击第二个怪物。如果它的生命值比第一个怪物高,我们就需要额外发射 a2a1 枚闪电来杀死它。否则,它已经死亡。在这两种情况下,第三个怪物会受到多少伤害?假设它的生命值很高。在第一种情况下,它会受到 a2 伤害,因为所有的闪电都会击中它。但在第二种情况下,它也会受到 a2 次伤害。以此类推。这就意味着 i 个怪物需要被 max(0,aiai1) 个闪电击中。

那么 k=1 的答案就等于 a1+i=2nmax(0,aiai1)

如何计算任意 k 的答案呢?事实上,两者的差别并不大。只需将每个怪物的健康值从 ai 改为 aik 即可,而前面所述的整个过程将保持不变。因此,任何 k 的答案都等于 a1k+i=2nmax(0,aikai1k)

这个结论也真的是妙!对于我来说,很难获得 30 分 n5000 算法。

继续优化。把 max 拆开,看每一个 aik 的系数,取决于两个条件:

  • 如果是 i=1aiai1 ,则系数增加 1
  • 如果是 i=nai<ai+1 ,则系数减少 1

我们把 i 怪兽的这个系数称为 ci 。因此,我们需要计算 i=1nciaik 。注意,ci 是固定的。

这是什么?数论分块,只不过是向上取整的数论分块,但是我们知道 nl=n1l+1,所以依然可以转化为下取整。

我们可以考虑每个 ai 对答案的贡献,比如当前极长 [l,r] 使得 ail=air,那么答案区间 [l,r] 整体就加上 ci×ail。这个也 tm 很妙!

#include <bits/stdc++.h>
using namespace std;

#define PII pair<int, int>
#define _for(i, a, b) for (int i = (a); i <= (b); i++)
#define _pfor(i, a, b) for (int i = (a); i >= (b); i--)
#define int long long
const int N = 3e5 + 5;

int n, a[N], maxn, ans[N];

signed main() {
	cin >> n;
	_for(i, 1, n) cin >> a[i], maxn = max(maxn, a[i]);
	_for(i, 1, n) {
		int cnt = 0;
		if (i == 1 || a[i] > a[i - 1]) cnt++;
		if (i < n && a[i + 1] > a[i]) cnt--;
		int l = 1, r;
		while (l <= a[i]) {
			int t = (a[i] - 1) / l;
			if (t) r = (a[i] - 1) / t;
			else r = a[i];
			ans[l] += cnt * (t + 1);
			ans[r + 1] -= cnt * (t + 1);
			l = r + 1; 
		} 
		ans[a[i] + 1] += cnt; // warning!
	}
	_for(i, 1, maxn) ans[i] += ans[i - 1];
	_for(i, 1, maxn) cout << ans[i] << ' ';
}

T4: ARC100E

题目描述

给你一个长度为 2n 的序列 a,每个 1K2n1,找出最大的 ai+ajiorjK0i<j<2n)并输出。
or 表示按位或运算。n18

题目思路

求出 iorjK 的最大值,等于说是求出等于 1,等于 2,一直到 k 的最大值。但是小于 k 的在之前的询问中求过,所以我们只需要把 res 放在外面就求出 K 的最大值。比如:

int res = 0;
for (int i = 1; i < ((1 << n) - 1); i++) {
	res = max(res, or=i的答案);
    cout << res << endl;
}

两个数或起来等于 K,那么这两个数肯定是 K 二进制表示的子集。定义 mxk 表示 k 二进制子集中 a 数组的最大值,mnk 表示 k 二进制子集中 a 数组的次大值。那么答案就是 mxK+mnK

预处理 mx 数组和 mn 数组,首先外层循环枚举 k,再枚举 k 的子集,花费 O(3n) 的时间,足以通过本题,440ms。

signed main() {
	cin >> n;
	_for(i, 0, (1 << n) - 1) a[i] = read();
	_for(i, 0, (1 << n) - 1) {
		for (int j = i; j; j = (j - 1) & i) {
			if (a[j] > tt[i]) tt2[i] = tt[i], tt[i] = a[j];
			else if (a[j] > tt2[i]) tt2[i] = a[j];
		}
		if (a[0] > tt[i]) tt2[i] = tt[i], tt[i] = a[0];
		else if (a[0] > tt2[i]) tt2[i] = a[0];
	}
	int res = 0;
	_for(i, 1, (1 << n) - 1) {
		res = max(res, tt[i] + tt2[i]);
		wr(res); putchar('\n');
	}
}

但是我们可以用一个更高效的方式,类似于 dp(被叫做高维前缀和)。就是 mxi 可以由 mxj 转移过来,其中 ji 的子集。当然,快多了,32 ms。

signed main() {
	cin >> n;
	_for(i, 0, (1 << n) - 1) a[i] = read(), tt[i] = a[i];
	_for(j, 0, n - 1) {
		_for(i, 0, (1 << n) - 1) {
			if (i >> j & 1) {
                // mx[i]由子集mx[i^(1<<j)]转移过来
				if (tt[i ^ (1 << j)] > tt[i]) tt2[i] = tt[i], tt[i] = tt[i ^ (1 << j)];
				else if (tt[i ^ (1 << j)] > tt2[i]) tt2[i] = tt[i ^ (1 << j)];	
			}
		}
    }
	int res = 0;
	_for(i, 1, (1 << n) - 1) {
		res = max(res, tt[i] + tt2[i]);
		wr(res); putchar('\n');
	}
}

T5: AGC030F

这个题也出的不错。

题目描述

有一个 2N 个数的序列 A,从 12N 标号。你要把 12N 这些数填进去,使它形成一个排列。

但是已经有一些位置强制填了特定的数了,输入时会给出。

最后令长度为 N 的序列 B 为:令 Bi=min{A2i1,A2i}

询问所有方案中能得到的不同的 B 的数量。 1N300

题目思路

把这 2N 个数两两配对,然后每一对中的较小数会进到 B 里面去。

把已经确定的配对 A2i1,A2i1,剩下的是一对中有一个确定了的,和两个都没确定的。

12N 这些数排成一排,配对的连一条线,那么 B 中的元素就是每条线左端点的值,但是 B 中还有顺序需要进行处理。

考虑:如果某一条线的某一端的值,在 A 中是存在的,也就是说这一对的一端已经是确定的,那么在 B 中的位置也是确定的。

如果这条线两端的值都没有在 A 中,那么我们先不给这条线赋值,等到最后统计完所有方案后,可以发现这样的线的个数是确定的(就等于 N 减去一端在 A 中的数对的数量),假设为 S,把答案乘以 S! 就行了。

那么,也就是说,我们需要统计连线方案数,两个方案不同当且仅当某个位置在其中一个方案中是线的左端点,而在另一个方案中不是,或者某个左端点所在的线的标号不同(如果这条线的一端在 A 中存在)。

vi 表示值 i 是否在 A 中存在。则从大到小 dp,设状态 dp(i,j,k) 表示考虑了 i 的值,其中有 jvx=0 的右端点还未配对,有 kvx=1 的右端点还未配对。

则有转移,dp(i+1,j,k) 可以转移给:

  • vi=1dp(i,j1,k),表示匹配了一个更大的 vx=0 的右端点。

  • vi=1dp(i,j,k+1),表示自己变成一个右端点。

  • vi=0dp(i,j1,k),表示匹配了一个更大的 vx=0 的右端点。

  • vi=0dp(i,j,k1),表示匹配了一个更大的 vx=1 的右端点。注意这里需要乘系数 k,且每一个带来的这条线的标号都不同。(注意理解)

  • vi=0dp(i,j+1,k),表示自己变成一个右端点。

时间复杂度 O(n3)。代码:

#include <bits/stdc++.h>
using namespace std;

#define PII pair<int, int>
#define _for(i, a, b) for (int i = (a); i <= (b); i++)
#define _pfor(i, a, b) for (int i = (a); i >= (b); i--)
#define int long long
const int N = 605, mod = 1e9 + 7;

int n, a[N], vis[N], tot;
int b[N], m, dp[2][N][N];

void add(int &a, int b) {
	a += b;
	if (a > mod) a -= mod;
}

signed main() {
	cin >> n;
	_for(i, 1, 2 * n) cin >> a[i];
	for (int i = 1; i <= 2 * n; i += 2) {
		int cnt = (a[i] == -1) + (a[i + 1] == -1);
		if (cnt == 1) {
			if (a[i] != -1) vis[a[i]] = 1;
			if (a[i + 1] != -1) vis[a[i + 1]] = 1;
		}
		if (cnt == 2) tot++;
		if (cnt == 0) vis[a[i]] = vis[a[i + 1]] = 2;
	}
	_pfor(i, 2 * n, 1) if (vis[i] <= 1) b[++m] = i;
//	cout << m << endl;
//	_for(i, 1, m) cout << b[i] << ' '; puts("");
	dp[0][0][0] = 1;
	_for(i, 1, m) {
		memset(dp[i & 1], 0, sizeof dp[i & 1]);
		_for(j, 0, n) _for(k, 0, n) {
			if (vis[b[i]] == 1) {
				add(dp[i & 1][j][k], dp[i - 1 & 1][j + 1][k]);
				if (k) add(dp[i & 1][j][k], dp[i - 1 & 1][j][k - 1]);
			}
			else {
				if (j) add(dp[i & 1][j][k], dp[i - 1 & 1][j - 1][k]);
				add(dp[i & 1][j][k], dp[i - 1 & 1][j + 1][k]);
				add(dp[i & 1][j][k], dp[i - 1 & 1][j][k + 1] * (k + 1) % mod);
			}
		}
	}
	int res = dp[m & 1][0][0];
	_for(i, 1, tot) res = res * i % mod;
	cout << res << endl;
}

T6: 弹飞绵羊

考试的题目是这题的加强版,只不过我觉得那题十分智慧,数据也十分友情,干脆不补了。

用分块做更简单。定义 cnti 表示从 i 开始,跳出 i 这个块的步数,pi 表示从 i 开始,跳出这个块能跳到的下标。

先对这两个数组进行预处理:

_pfor(i, n, 1) {
	if (i + a[i] > rx[bel[i]]) {
		cnt[i] = 1;
		p[i] = i + a[i];
	}
	else {
		cnt[i] = cnt[i + a[i]] + 1; // 递推
		p[i] = p[i + a[i]];
	}
}

如果是单点修改,那么直接对这个数所在块,仿照上述方法重新更新一遍就行。

while (m--) {
	int op, x, y;
	cin >> op >> x;
	x++; 
	if (op == 1) {
		int res = 0, t = x;
		while (t <= n) {
			res += cnt[t];
			t = p[t];
		}
		cout << res << endl;	
	}
	else {
		cin >> y;
		a[x] = y;
		_pfor(i, rx[bel[x]], lx[bel[x]]) {
			if (i + a[i] > rx[bel[i]]) {
				cnt[i] = 1;
				p[i] = i + a[i];
			}
			else {
				cnt[i] = cnt[i + a[i]] + 1;
				p[i] = p[i + a[i]];
			}
		}
	}
}
posted @   Otue  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示