Educational Codeforces Round 88 (Rated for Div. 2)
https://codeforces.com/contest/1359
A - Berland Poker
随便平均分配一下。
B - New Theatre Square
比A还水。但是以后要注意,不要尝试节省不必要的空间,能不优化就不优化。
*C - Mixing Water
题意:给两种水,各无限杯,热水温度为 \(h\) ,冷水温度为 \(c\) ,你只能轮流倒热水和冷水并且必须先倒热水。要求倒尽可能少的杯数使得总温度接近 \(t\) ,总温度是所有温度的平均,也就是,假如倒了 \(x\) 杯热水和 \(y\) 杯冷水,则总温度为 \(\frac{xh+yc}{x+y}\) 。
题解:显然若 \(t\leq \frac{h+c}{2}\) ,则必须倒两杯,否则要么倒两杯,要么倒奇数杯( \(k+1\) 杯热水和 \(k\) 杯冷水)。设一开始先倒入一杯热水,则温度为 \(h\) ,这之后若每次倒入一杯热水和一杯冷水,温度会变为 \(\frac{(k+1)h+kc}{2k+1}\) ,这个式子显然趋向于 \(\frac{h+c}{2}\) ,直觉上感觉到这个应该是单调递减的,不放心可以临项作差验证。既然是单调的那么就可以使用二分、倍增等算法去做,我选择的算法是倍增。我要求保持总的温度始终大于等于 \(t\) ,那么根据倍增的想法,若往前走 \(k\) 步后总温度仍然大于等于 \(t\) 则往前走 \(k\) 步,否则不走,无论走还是不走下一次都是走 \(k/2\) 步。但是这个算法在遇到某些数据的时候会出错。无所谓,我们找出一个大概的 \(k\) ,然后在其附近20个温度找一个最小的就可以了,最后别忘记和只倒两杯比较一下。
ll h, c, t;
void TestCase() {
scanf("%lld%lld%lld", &h, &c, &t);
ll ans = 0;
for(ll step = 1ll << 20; step >= 1ll; step >>= 1ll) {
ll k = ans + step;
if((2ll * k + 1ll)*t <= (k + 1ll)*h + k * c)
ans = k;
}
ll cur = 2ll;
long double curdif = fabs(((long double)h + (long double)c) / 2.0 - (long double)t);
for(ll i = max(0ll, ans - 10ll); i <= ans + 10ll; ++i) {
long double tmp = (long double)((i + 1ll) * h + i * c) / (long double)((2ll * i + 1ll));
if(fabs(tmp - t) < curdif) {
curdif = fabs(tmp - t);
cur = 2ll * i + 1ll;
}
}
printf("%lld\n", cur);
return;
}
用了long double,毕竟有绝对值的真的不知道咋比较谁更近。传闻double都有8位十进制精度,那么long double对付这题应该是游刃有余的。
*D - Yet Another Yet Another Task
题意:给一个 \(n\) 个数的数组,这些数字都很小(绝对值不超过30),Alice先选择一段非空连续区间 \([l,r]\) ,Bob选择其中最大的一个元素去掉,剩下的是Alice的得分。最大化Alice的得分。
题解:一开始想了很多算法,感觉就很不合理,比如计算某个元素为最大值时的结果,这样很容易退化。首先假如没有Bob,这道题就是经典的“最大子数组和”。其实这题目要从值域入手,假设我们取到的最大值为 \(x\) ,则大于 \(x\) 的都不能包含进来,直接设为负无穷,然后求一次最大子数组和,然后把最大子数组和减去 \(x\) 并尝试更新答案。这里有个小问题就是这次的最大子数组和未必真的包含 \(x\) ,但是容易知道这时一定不是最优答案,最优答案一定至少会是在这次的最大子数组和里面的最大元素作为 \(x\) 的时候更新。
提示:最大子数组和的解法:设 \(dp[i]\) 为必须以第 \(i\) 个位置结尾的最大得分。
int n;
int a[100005];
ll dp[100005];
void TestCase() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
ll ans = 0;
for(int x = 30; x > 0; --x) {
dp[0] = 0;
for(int i = 1; i <= n; ++i) {
if(a[i] > x)
a[i] = -INF;
dp[i] = max(0ll, dp[i - 1]) + a[i];
ans = max(ans, dp[i] - x);
}
}
printf("%lld\n", ans);
return;
}
*E - Modular Stability
题目:计数题,给一个 \(n,k\) ,计算满足下面条件的数组的数量:
- 是 \([1,n]\) 中的 \(k\) 个不同的整数,且严格升序。
- 对任意的 \(x\) ,这个数组无论按照什么顺序打乱之后,从左到右取模,到最后的结果一样。
题解:很明显,若选择 \(d\) 和 \(d\) 的倍数,那么取模得到的结果最后制约条件只有 \(d\) 。所以就是 \(\sum\limits_{d=1}^{n} C_{\lfloor\frac{n}{d}\rfloor-1}^{k-1}\) 。否则,就一定满足,这 \(k\) 个数的gcd(记为 \(g\) )没有被选中,这时一个结果不同的构造是:若是这种情况,则存在 \(i>1\) 的 \(a_i\) 不是 \(a_1\) 的倍数,则令 \(x=a_i\) 有 \(x \mod a_1 = a_i \mod a_1\) ,由于 \(a_i\) 不是 \(a_1\) 的倍数所以这个不为0,而第二种情况 \(x \mod a_i\) 为0,不等,但好像这就能说明不对了吗?暂时没有理解。
写了这个东西来验证猜想,通过了大数据。
ll qpow(ll x, ll n) {
ll res = 1;
while(n) {
if(n & 1)
res = res * x % MOD;
x = x * x % MOD;
n >>= 1;
}
return res;
}
ll C(ll n, ll m) {
ll up = 1, down = 1;
for(int i = 1; i <= m; ++i)
down = down * i % MOD;
for(int i = 1; i <= m; ++i)
up = up * (n - i + 1) % MOD;
return up * qpow(down, MOD - 2) % MOD;
}
void TestCase() {
int n, k;
scanf("%d%d", &n, &k);
ll sum = 0;
for(int i = 1; i <= n; ++i)
sum += C((n / i) - 1, k - 1);
sum %= MOD;
printf("%lld\n", sum);
return;
}
然后这个东西感觉可以用线段树优化。因为每次是一段连续区间的求积。
struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
static const int MAXN = 500000;
ll st[(MAXN << 2) + 5];
void PushUp(int o) {
st[o] = st[ls] * st[rs] % MOD;
}
void Build(int o, int l, int r) {
if(l == r)
st[o] = l;
else {
int m = l + r >> 1;
Build(ls, l, m);
Build(rs, m + 1, r);
PushUp(o);
}
}
ll Query(int o, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) {
return st[o];
} else {
int m = l + r >> 1;
ll res = 1;
if(ql <= m)
res = Query(ls, l, m, ql, qr);
if(qr >= m + 1)
res = res * Query(rs, m + 1, r, ql, qr) % MOD;
return res;
}
}
#undef ls
#undef rs
} st;
ll qpow(ll x, ll n) {
ll res = 1;
while(n) {
if(n & 1)
res = res * x % MOD;
x = x * x % MOD;
n >>= 1;
}
return res;
}
ll inv_down;
void calc_inv_down(int m) {
ll down = 1ll;
for(int i = 1; i <= m; ++i)
down = down * i % MOD;
inv_down = qpow(down, MOD - 2);
return;
}
int n, k;
ll C(int _n, int _m) {
if(_n < _m)
return 0;
ll up = st.Query(1, 1, n, _n - _m + 1, _n);
return up * inv_down % MOD;
}
void TestCase() {
scanf("%d%d", &n, &k);
if(k == 1) {
printf("%d\n", n);
return;
}
st.Build(1, 1, n);
ll sum = 0;
calc_inv_down(k - 1);
for(int i = 1; i <= n; ++i)
sum += C((n / i) - 1, k - 1);
sum %= MOD;
printf("%lld\n", sum);
return;
}
然后看一下别人的题解,好像不需要这么复杂,直接粘贴组合数的模板就可以了。要记得(模为质数时的)组合数 \(C_n^k\) 在 \(O(n)\) 预处理之后每次回答就是 \(O(1)\) 的。只有当模不为质数,或者模数不够大时,乘法逆元才有可能不存在,这时才需要用维护幺半群的线段树,其他时候直接使用维护交换群的数据结构就可以。
const int MAXN = 1e6;
ll inv[MAXN + 5], fac[MAXN + 5], invfac[MAXN + 5];
void init_C(int n) {
inv[1] = 1;
for(int i = 2; i <= n; i++)
inv[i] = inv[MOD % i] * (MOD - MOD / i) % MOD;
fac[0] = 1, invfac[0] = 1;
for(int i = 1; i <= n; i++) {
fac[i] = fac[i - 1] * i % MOD;
invfac[i] = invfac[i - 1] * inv[i] % MOD;
}
}
ll C(ll n, ll m) {
if(n < m)
return 0;
return fac[n] * invfac[n - m] % MOD * invfac[m] % MOD;
}
int n, k;
void TestCase() {
scanf("%d%d", &n, &k);
init_C(n);
ll sum = 0;
for(int i = 1; i <= n; ++i)
sum += C((n / i) - 1, k - 1);
sum %= MOD;
printf("%lld\n", sum);
return;
}