【校内测试】2020-7-13校内测试

2020-7-13校内测试总结+题解。

比赛链接

点击打开链接

比赛经历

打开 A 题,发现是个套路拆位。15分钟码完,搁着,不想对拍。

打开 B 题,发现是个 dp 或者贪心之类的。搁着,去看 C

打开 C 题,发现数据 \(n\le 10^9\) 感觉是个矩阵乘法(巧了我恰好也讲矩阵乘法)。发现 80pts 比较简单。但还没有想好如何用矩阵优化。

回来看 B 题,写了一个 \(\mathcal O(n^2)\) 的暴力和一个 \(C_i=1\)\(\mathcal O(n)\) 的贪心部分分。仔细想想,发现这转移好像是一次函数的转移,那么最优转移应该是凸包上的点。想到一个感觉方向正确办法,先搁着,写 C 的暴力。

再看 C 题,写了一个 80 分的暴力。然后开始码 B 题的“方向正确的办法”。

考完发现方向正确的办法是错的(但方向的确正确),C 题的暴力写挂了,身败名裂。

A-幸运值

题目解法

常见套路。

首先拆位,然后对于每一位都转化为只有 0/1 的问题。所以只要有奇数个 1 这一位就有贡献。考虑枚举有多少个 1,然后使用组合数计算即可。

时间复杂度 \(\mathcal O(n\log_2 n)\)

代码

#include 
using namespace std;
typedef long long ll;
const int CN = 1e5 + 5, MOD = 998244353;
int ad(int x, int y) { return ((x + y) > MOD) ? (x + y - MOD) : (x + y); }
int dc(int x, int y) { return ((x - y) < 0) ? (x - y + MOD) : (x - y); }
int ml(int x, int y) { return (ll) x * y % MOD; }
int ksm(int x, int y) {
	int ret = 1;
	for (; y; y >>= 1, x = ml(x, x))
		if (y & 1) ret = ml(ret, x);
	return ret;
}
int N, K, A[CN], fac[CN], ifac[CN];
void Init() {
	fac[0] = 1;
	for (int i = 1; i <= N; ++i) fac[i] = ml(fac[i - 1], i);
	for (int i = 0; i <= N; ++i) ifac[i] = ksm(fac[i], MOD - 2);
}
int binom(int x, int y) {
	if (x < 0 || y < 0 || x < y) return 0;
	return ml(fac[x], ml(ifac[y], ifac[x - y]));
}
int main() {
	scanf("%d%d", &N, &K);
	Init();
	for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
	int ans = 0;
	for (int i = 0; i <= 30; ++i) {
		int cnt[] = {0, 0};
		for (int j = 1; j <= N; ++j) ++cnt[(A[j] >> i) & 1];
		for (int num = 1; num <= K; num += 2) {
			int res = K - num;
			ans = ad(ans, ml((1 << i) % MOD, ml(binom(cnt[1], num), binom(cnt[0], res))));
		}
	}
	printf("%d\n", ans);
	return 0;
}

B-公交运输

题目解法

下文中,决策通常指“决策点”。决策点就是“转移点”。例如:\(f_i\)\(j\) 转移,那么 \(j\) 就是 \(f_i\) 决策点。

最优决策指所有决策中使结果最小的决策。

不难想到想到一个暴力:

\(f_i\) 表示到 \(i\) 就换乘的最小代价。暴力枚举 \(1\le j < i-1\) ,转移即可。
具体地,这样转移:

\[f_i=\min_{j=1}^{i-1}\{f_j+V_j\cdot\frac{i-j}{C_j}\} \]

当然转移的时候需要判断 \(i\) 是否可以走到 \(j\)

可以发现转移是以 \(i\) 为主元是一个一次函数(\(j\) 固定)。所以最优决策是一个凸包的样子,维护加入直线和求凸包上点值,李超树维护即可。

而且复杂度应该是 \(\mathcal O(n\cdot maxc\cdot \log^2n)\) 的,明显过不了。

可以发现一个性质:如果 \(i < j\) 并且 \(V_i \ge V_j\) ,那么必然从 \(V_j\) 转移而不是从 \(V_i\) 转移(当然是在即能从 \(i\) 也能从\(j\) 转移的前提下)。这是因为走到 \(i\) 后必然可以走到 \(j\) ,所以换乘一下就好了。

考虑维护一个决策的单调栈,每次加入一个决策就将所有栈顶 \(V\) 比当前决策的 \(V\) 大的决策弹出。

现在先意会一下最优决策的情况:对于一个 \(V\) 比较大的决策 \(j\),只可能在 \(i\)\(j\) 比较近的时候最为 \(i\) 的最优决策。对于一个 \(V\) 比较小的决策 \(j\) ,虽然有可能在 \(i\)\(j\) 比较近的地方不选择决策 \(j\) ,但是 \(i\) 比较大的时候就会选择了。

那么正解就呼之欲出了。我们只要知道两个决策的分界点在哪里即可,也就是将两个一次函数求交点。所以每次保证三个事情:

  • 栈内斜率单调
  • 相邻两直线交点单调
  • 栈顶两直线交点 \(j\) 满足 \(j\(f_i\) 是当前需要计算的值)

一个点进一次,出一次。时间复杂度 \(\mathcal O(n\cdot maxc)\)

代码

#include 
#include 
#include 
#include 
typedef double db;
typedef long long ll;
using namespace std;
const int CN = 1e6 + 5, inf = 0x3f3f3f3f;
const db eps = 1e-7;
int N, maxc, C[CN], V[CN], f[CN];
vector kind[11][11];
bool judge(int i, int j, int t) {
	ll k1 = V[i], k2 = V[j];
	ll b1 = (ll)f[i] * C[j] - (ll)i * V[i];
	ll b2 = (ll)f[j] * C[j] - (ll)j * V[j];
	if (k1 > k2) return (b2 - b1) <= t * (k1 - k2);
	else return (b2 - b1) >= t * (k1 - k2);
}
int transfer(int i, int j) { return f[j] + (i - j) / C[j] * V[j]; }
bool cross(int i, int j, int k) {
	ll k1 = V[i], k2 = V[j], k3 = V[k];
	ll b1 = (ll)f[i] * C[j] - (ll)i * V[i];
	ll b2 = (ll)f[j] * C[j] - (ll)j * V[j];
	ll b3 = (ll)f[k] * C[k] - (ll)k * V[k];
	return (b1 - b2) * (k3 - k2) < (b2 - b3) * (k2 - k1);
}
int main() {
	memset(f, 0x3f, sizeof(f));
	scanf("%d%d", &N, &maxc);
	for (int i = 0; i < N; ++i)
		scanf("%d%d", &C[i], &V[i]);
	kind[C[0]][0].push_back(0);
	f[0] = 0;
	for (int i = 1; i <= N; ++i) {
		f[i] = inf;
		for (int j = 1; j <= maxc; ++j) {
			vector &v = kind[j][i % j];
			if (!v.empty()) {
				while (v.size() >= 2 && judge(v[v.size() - 2], v.back(), i))
					v.pop_back();
				f[i] = min(f[i], f[v.back()] + (i - v.back()) / j * V[v.back()]);
			}
		}
		if (f[i] == inf) printf("-1\n");
		else printf("%d\n", f[i]);
		if (i != N) {
			vector &v = kind[C[i]][i % C[i]];
			while (!v.empty() && V[i] <= V[v.back()])
				v.pop_back();
			while (v.size() >= 2) {
				int p1 = v[v.size() - 2], p2 = v.back();
				if (!cross(p1, p2, i))
					break ;
				v.pop_back();
			}
			v.push_back(i);
		}
	}
	return 0;
}

总结

  • 这个套路记住。类似题目有 [JSOI2011]柠檬 (考试的时候居然没有想起来)。其实这题还是蛮套路的呢。

C-密码

题目解法

所有匹配可以分为两类:

  • 完全被前 \(m\) 个字符串包含的。
  • 交错的在两个拼起来的字符串之间,但是因为长度问题并不可能跨了三个字符串。

\(f_i\) 表示第 \(i\) 个字符串中给定字符串出现次数,\(f_i\) 的转移是一个递推式+常数。下面就是处理拼起来的情况。为了计算相邻两个字符串之间产生的贡献,考虑记录每个字符串首尾分别是哪个单位字符串(单位字符串为题目给定的字符串)。那么预处理出来任何两个单位字符串的之间的贡献。

考虑这是个递推式,并且 \(n\le 10^9\) 所以需要使用矩阵乘法优化转移。

代码

但如何优化还没想好,更别说代码了。

总结

代码还没写好,更别说总结了。

你可能感兴趣的:(【校内测试】2020-7-13校内测试)