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\) ,转移即可。
具体地,这样转移:
当然转移的时候需要判断 \(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\) 所以需要使用矩阵乘法优化转移。
代码
但如何优化还没想好,更别说代码了。
总结
代码还没写好,更别说总结了。