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})

题解:显然若 (tleq 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
", 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
", ans);
    return;
}

*E - Modular Stability

题目:计数题,给一个 (n,k) ,计算满足下面条件的数组的数量:

  1. ([1,n]) 中的 (k) 个不同的整数,且严格升序。
  2. 对任意的 (x) ,这个数组无论按照什么顺序打乱之后,从左到右取模,到最后的结果一样。

题解:很明显,若选择 (d)(d) 的倍数,那么取模得到的结果最后制约条件只有 (d) 。所以就是 (sumlimits_{d=1}^{n} C_{lfloorfrac{n}{d} floor-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
", 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);
        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
", 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
", sum);
    return;
}
原文地址:https://www.cnblogs.com/KisekiPurin2019/p/13044667.html