25暑假第一周做题记录

前言

暑假第一周的杂题以建模和 dp 为主。

烟花表演

这个题首先有一个非常一眼的 dp:设 \(f_{u,i}\) 表示 \(u\) 子树长度全部为 \(i\) 的代价。然后可以写出转移方程:

\[f_{u,i}=\sum\min_{j\le i}\{f_{v,j}+|w-i+j|\} \]

但是这个东西的复杂度是 \(\mathcal O(n\left(\sum w\right)^2)\) 的,考虑优化。

首先我们将 \(u\) 的所有点值写成函数形式 \(f_u(x)\),现在我们就去考虑 \(f_v(y)\)\(f_u(x)\) 的贡献。你考虑每次都是一个 \(\min+\) 卷积的形式并且里面是绝对值函数,所以合理推测 \(f\) 是下凸的。

具体的,首先 \(f\) 一定由若干段组成,并且每段斜率单调递增且为整数。并且一定会有一段的斜率取值为 0,所以对 \(f_v(y)\) 斜率为 0 的这段,我们记为 \([L,R]\),我们现在考虑将 \(f_v\) 贡献到 \(f_u\) 考虑每一段的变化情况,假设 \(e(u,v)=w,F_v(x)=\min\limits_{y\le x}\{f_v{y}+|w-x+y|\}\),分讨有:

  • \(x<L\) 时,也就是当 \(x\) 还很小的时候,最小值一定是把 \(w\) 改成 0,因为左边的斜率很小,所以如果不将绝对值变小,那么 \(f_v\) 增加的一定更多,假设是 \(w+t\),那么函数会增加 \(kt,k\ge1\)。于是有 \(F_v(x)=f_v(x)+w\)
  • \(x>R+w\) 时,也就是 \(x\) 很大的时候,和上述情况相似。于是有 \(F_v(x)=f_v(R)+x-R-w\)
  • \(L\le x\le R+w\) 时我们继续分两种情况考虑:
    • \(L\le x\le L+w\) 时,有 \(F_v(x)=f_v(L)+L+w-x\),考虑最优情况不会随长度改变而改变。
    • \(L+w<x\le R+w\) 时,有 \(F_v(x)=f_v(R)+x-R-w\),原因一样,也是因为在最优的位置不需要考虑修改带来的影响。

现在我们只用考虑维护这个函数即可得到最后的答案。我们分成上述 4 种情况,在此不赘述。有的操作是区间平移,斜率修改。

我们注意到一个事实,就是 \(F\) 是连续的,这启发我们维护函数的拐点。于是我们修改 \(F\) 只需要把 \(L\) 及以后的拐点全删掉再新加入两个点即可。注意维护拐点的时候不需要去重,这样同一拐点的个数就是此处的斜率。

最后我们再来考虑实际实现的问题。首先因为拐点横坐标单调下降所以我们用可并堆维护函数。对于一个 \(u\) 我们会合并 \(deg_u-1\) 次函数,所以我们会有相同个数的斜率为正的拐点,于是我们每次就删掉这么多拐点即可。初始的函数就是所有边的权值和,然后每次修改斜率差都是 1,所以很好维护。

int n, m, rt[N], nd;
int fa[N], a[N], deg[N], ch[N << 1][2], d[N << 1];
ll val[N << 1], ans;

inline int adnd(ll x){val[++nd] = x; d[nd] = 0; return nd;}
inline int mrg(int x, int y){
    if(! x or ! y)return x | y; if(val[x] < val[y])swap(x, y);
    ch[x][1] = mrg(ch[x][1], y); if(d[ch[x][0]] < d[ch[x][1]])swap(ch[x][0], ch[x][1]);
    return d[x] = d[ch[x][1]] + 1, x;
}
inline void pop(int &x){x = mrg(ch[x][0], ch[x][1]);}

const string FileName = "";
signed main(){
    // fileio(FileName);
    n = rd(), m = rd();
    for(int i = 2; i <= n + m; ++i){
        fa[i] = rd(), a[i] = rd();
        ans += a[i]; ++deg[fa[i]];
    }
    for(int i = n + m; i > 1; --i){
        if(i <= n)while(--deg[i])pop(rt[i]);
        ll y = val[rt[i]]; pop(rt[i]);
        ll x = val[rt[i]]; pop(rt[i]);
        rt[i] = mrg(rt[i], mrg(adnd(x + a[i]), adnd(y + a[i])));
        rt[fa[i]] = mrg(rt[fa[i]], rt[i]);
    }
    while(deg[1]--)pop(rt[1]); while(rt[1])ans -= val[rt[1]], pop(rt[1]);
    return wt(ans), 0;
}

猎人杀

这道题的难点在于发现每个猎人被射中的概率会改变。但是话又说回来,这道题的关键就在于你发现一个神秘结论:一个猎人在存活猎人中被射死的概率等价于其在所有猎人(包括死亡的猎人)中被射死的概率,如果射中了死亡的猎人就继续开枪。

这个东西是很反直觉的,但是我们可以通过数学去证明它。

证明:

假设所有猎人的 \(\sum w=P\),当前存活的猎人的 \(\sum w=Q\),对于猎人 \(i\) 下一次被射死的概率为:\(p_i={w_i\over Q}\)

转化后的概率为:

\[p_i={w_i+(P-Q)p_i\over P}={w_i\over Q} \]

可以发现这两个概率相等所以这两个事件可以互相转换。

现在考虑转换后的问题。如果现在我们直接考虑 1 号猎人最后被射死的概率显然不好做于是考虑容斥。我们考虑在 1 号后面死的猎人,假设枚举的集合为 \(S\),设 \(sum=\sum\limits_{x\in S}w_x\),我们有:

\[ans=\sum_S(-1)^{|S|}{w_1\over P}\sum_{i=0}^{\infty}(1-{sum+w_1\over P})^i \]

因为最后面的 \(\sum\) 里面是收敛的所以有:

\[\begin{aligned} ans &=\sum_S(-1)^{|S|}{w_1\over P}{1\over1-(1-{sum+w_1\over P})}\\ &=\sum_S(-1)^{|S|}{w_1\over sum+w_1} \end{aligned} \]

所以答案只与 \(|S|\)\(sum\) 有关,这是背包,但是我们不能直接做不然会炸,我们直接经典生成函数求背包,设一个物品 \(i\) 的生成函数为 \(1+x^{w_i}\) 就可以做了。但是考虑到这个式子前面还带有 \((-1)^{|S|}\) 如果正常做背包最后还需要同时记录物品数和价值和,估计需要二元生成函数,太麻烦了!于是我们考虑将 \((-1)^{|S|}\) 放进生成函数中,于是重新令 \(i\) 的生成函数为 \(1-x^{w_i}\),然后就需要一个分治 NTT 即可。

railroad

考虑图论建模。我们可以先离散化,对每个速度建一个点,因为从小到大连边不需要代价于是就顺次连即可。因为需要跑完所有的特殊边我们考虑欧拉回路,于是建立一个巨大的虚点作为起点免费连向最小的点。连完后这张图大概率没有欧拉回路,于是就需要你添加一些边在保证有欧拉回路的情况下的最小代价。

不难发现我们要添加相邻两点的边当且仅当覆盖了这条边的向左的边和向右的边数量不一致。这个东西我们可以差分记一下,处理完这些后就只可能剩下若干度数为奇数的点,我们需要把这些点匹配,于是求一下最小生成树即可。

signed main(){
    // fileio(FileName);
    n = rd(), typ = rd();
    for(int i = 1, u, v; i <= n; ++i)b[++m] = u = rd(), b[++m] = v = rd(), E[i] = {u, v};
    b[++m] = inf, b[++m] = 1; E[++n] = {inf, 1};
    sort(b + 1, b + 1 + m); m = unique(b + 1, b + 1 + m) - b - 1;
    for(int i = 1; i <= m; ++i)fa[i] = i;
    for(int i = 1; i <= n; ++i){
        E[i].u = lower_bound(b + 1, b + 1 + m, E[i].u) - b;
        E[i].v = lower_bound(b + 1, b + 1 + m, E[i].v) - b;
        ++d[E[i].u]; --d[E[i].v]; mrg(E[i].u, E[i].v);
    }
    for(int i = 1; i <= m; ++i){
        d[i] += d[i - 1]; if(! d[i] or i == m)continue;
        mrg(i, i + 1); if(d[i] > 0)ans += d[i] * (b[i + 1] - b[i]);
    }
    for(int i = 1; i < m; ++i)if(fd(i) != fd(i + 1))e[++tot] = {i, i + 1, b[i + 1] - b[i]};
    sort(e + 1, e + 1 + tot);
    for(int i = 1; i <= tot; ++i){
        int u = fd(e[i].u), v = fd(e[i].v);
        if(u != v){fa[u] = v; ans += e[i].w;}
    }
    return wt(ans), 0;
}

CF1753D

你考虑一个椅子的移动次数小于 2 次,不然一定不优。于是我们分讨一次移动所产生的代价,然后可以花费 \(O(n^2)\) 去枚举终点(目标点),每次跑 dij 即可。

signed main(){
    ios :: sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    cin >> n >> m >> A >> B;
    for(int i = 1; i <= n; ++i)id[i].resize(m + 1), a[i].resize(m + 1);
    for(int i = 1; i <= n; ++i)for(int j = 1; j <= m; ++j)cin >> a[i][j], id[i][j] = ++tot;
    for(int i = 1; i <= n; ++i)for(int j = 1; j <= m; ++j){
        d[id[i][j]] = INF;
        if(a[i][j] == '#')continue;
        if(a[i][j] == '.')d[id[i][j]] = 0, q.push(make_pair(0, id[i][j]));
        if((a[i][j] == 'U' or a[i][j] == 'L') and chk(i + 1, j + 1))g[id[i + 1][j + 1]].push_back(make_pair(id[i][j], A));
        if((a[i][j] == 'U' or a[i][j] == 'R') and chk(i + 1, j - 1))g[id[i + 1][j - 1]].push_back(make_pair(id[i][j], A));
        if((a[i][j] == 'D' or a[i][j] == 'L') and chk(i - 1, j + 1))g[id[i - 1][j + 1]].push_back(make_pair(id[i][j], A));
        if((a[i][j] == 'D' or a[i][j] == 'R') and chk(i - 1, j - 1))g[id[i - 1][j - 1]].push_back(make_pair(id[i][j], A));
        if(a[i][j] == 'U' and chk(i + 2, j))g[id[i + 2][j]].push_back(make_pair(id[i][j], B));
        if(a[i][j] == 'D' and chk(i - 2, j))g[id[i - 2][j]].push_back(make_pair(id[i][j], B));
        if(a[i][j] == 'L' and chk(i, j + 2))g[id[i][j + 2]].push_back(make_pair(id[i][j], B));
        if(a[i][j] == 'R' and chk(i, j - 2))g[id[i][j - 2]].push_back(make_pair(id[i][j], B));
    }
    dij(); ll res = INF;
    for(int i = 1; i <= n; ++i)for(int j = 1; j <= m; ++j){
        if(chk(i + 1, j))Min(res, d[id[i][j]] + d[id[i + 1][j]]);
        if(chk(i, j + 1))Min(res, d[id[i][j]] + d[id[i][j + 1]]);
    }
    cout << (res == INF ? - 1 : res);
    return 0;
}

CF1404E

经典的二分图题目。考虑一个长条分很多次小长条放一定不优(显然),于是考虑在这些长条的端点处建点,然后对于每个点的分配我们可以拆点跑二分图匹配即可。

CF1827C

牛逼题,考虑凸轮剑魔。我们把第 \(i\) 个位置与第 \(i+1\) 个位置之间的空隙看成第 \(i\) 个点,对于一个偶回文串 \(S[l,r]\) 我们连边 \((l-1,r)\)。于是我们就能够得到若干连通块,考虑答案即为:

\[\sum_{S\subseteq G} {|S|\choose2} \]

证明考虑 border 的一些性质即可。然后就是如何维护连通块,我们考虑这道题的 trick 即可。

CF724E

模拟最大流板子。

梦幻岛宝珠

背包。考虑每个物品都能被写成 \(k\times2^t\) 的形式于是我们按二进制下的位数从小到大考虑,并且需要考虑进位的贡献。所以有设 \(g_{i,j}\) 表示考虑到第 \(i\) 位体积为 \(j\) 的最大价值,转移正常做即可。最后考虑如何统计答案?考虑把 \(V\) 的限制分成最高位以及其他位,因为最高位体积为 1。我们设 \(f_{i,j}\) 表示考虑前 \(i\) 位时选择了体积为 \(j\times2^i\) 的上界,有转移:

\[f_{i,j}=\max_{k\le j}(f_{i,j},f_{i-1,(j-k)*2+V_{i-1}}+g_{i,k}) \]

最后答案就是 \(f_{m,1}\),其中 \(m\)\(V\) 的二进制下的位数。

基础 ABC 练习题

神仙题,弱化版是 AGC055D

首先观察数据范围,发现 \(n\) 很小于是考虑高维 dp 或者 状压 dp。在 dp 前我们需要去寻找一些性质,考虑怎么样的序列是合法的?我们尝试用一些必要条件去刻画它,这里引用某选手说过的一句话:必要条件多了就可以变成充要条件。

我们可以观察三个不同的子序列:ABCBCACAB。因为这些子序列都有相同的字母但是这些字母的位置不同,所以我们尝试去研究不同字母之间的位置关系。能够发现:只有在 ABC 中,A 才会出现在 C 的前面,否则其在 C 的后面。对于其他两对字母也存在类似的关系。设 \(pre_a,pre_b,pre_c\) 分别表示前缀 A,B,C 的个数,我们可以发现对于任意前缀都有:\(pre_a-pre_c\le cnt_{ABC}\)。其他字母同理。

由此我们得到一个神秘的条件:

\[\max(pre_a-pre_c)+\max(pre_b-pre_a)+\max(pre_c-pre_b)\le n \]

现在如果我能够通过上述条件构造出合法的解,那么这个条件就为充要条件,于是我们尝试构造一个合法的序列。首先对于一个序列,如果其合法,那么我们将下标平移不会影响其合法性。比如我们把最开始的字母移到最后,可以发现:这个字母所在的子序列进行了一次轮换,其余子序列不变。所以新的序列一定是合法的。于是我们考虑将序列连成一个环,下面给出构造方案:对于环上第 \(i\)A,对应环上第 \(i+cnt_{BCA}\)B 和环上第 \(i+cnt_{BCA}+cnt_{CAB}\)C

于是现在我们需要证明的是:对于任意的 \(i\),环上第 \(i+cnt_{BCA}+cnt_{CAB}\)C 不在环上第 \(i\)A 和环上第 \(i+cnt_{BCA}\)B 之间。

证明是容易的。我们考虑从第 \(i\)A 处断环成链,于是我们需要证明的是原第 \(i+cnt_{BCA}+cnt_{CAB}\)C 的位置小等于 \(3n\)。考虑到原第 \(i+cnt_{BCA}+cnt_{CAB}\)C 在新的序列中处于第 \(1+cnt_{BCA}+cnt_{CAB}\)C,考虑最坏的情况是所有 C 都在 A,B 后面,因为有 \(cnt_{ABC}+cnt_{BCA}+cnt_{CAB}=n\) 并且因为新的序列开头是 A 所以 \(cnt_{ABC}>0\),进一步得到 \(1+cnt_{BCA}+cnt_{CAB}\le n\),于是得证。

对于弱化版,当 \(n\le15\) 时,我们可以直接 \(\mathcal O(n^6)\) dp 直接记录所有信息即可。具体的,设 \(f_{i,j,k,a,b,c}\) 维护前缀 A,B,C 的数量以及 \(cnt_{ABC},cnt_{BCA},cnt_{CAB}\) 然后 \(\mathcal O(1)\) 转移。

考虑 \(n\le 60\) 时,我们尝试干掉一维状态。因为有 \(cnt_{ABC}+cnt_{BCA}+cnt_{CAB}=n\) 所以我们可以枚举 \(cnt_{ABC}\)\(cnt_{BCA}\) 从而确定 \(cnt_{CAB}\),但是考虑到直接做等于 \(cnt\) 的限制不太好 dp,于是我们改成做 \(\le cnt\) 的限制最后差分一下即可,时间复杂度 \(\mathcal O(n^5)\)

inline unsigned calc(int a, int b, int c){
    if(min(a, b) < 0)return 0; memset(f, 0, sizeof f); f[0][0][0] = 1;
    for(int i = 0; i <= n; ++i)for(int j = 0; j <= min(n, i + b); ++j)
        for(int k = max(0, i - a); k <= min(n, j + c); ++k)if(i + j + k != 3 * n){
            int pos = i + j + k;
            if(s[pos] == '?' or s[pos] == 'A')f[i + 1][j][k] += f[i][j][k];
            if(s[pos] == '?' or s[pos] == 'B')f[i][j + 1][k] += f[i][j][k];
            if(s[pos] == '?' or s[pos] == 'C')f[i][j][k + 1] += f[i][j][k];
        }
    return f[n][n][n];
}

void sol(){
    cin >> n >> ss[0] >> ss[1] >> s; ans = 0;
    if(n != 60)return cout << - 1 << endl, void();
    for(int a = 0; a <= n; ++a)for(int b = 0; a + b <= n; ++b){
        int x = a, y = b; while(x <= n and ss[0][x] == '0')++x;
        while(y <= n and ss[1][y] == '0')++y; int c = n - x - y;
        if(c < 0)continue; ans += calc(a, b, c) - calc(a - 1, b, c) - calc(a, b - 1, c) + calc(a - 1, b - 1, c);
    }cout << ans  << endl;
}

CF1519F

Hall 定理的应用好题!

考虑我们要求的东西有一个限制:

\[\sum a_i\le\sum b_j \]

这玩意和 Hall 定理很像,但是 Hall 定理描述的对象是二分图的点,但是这道题的限制却是二分图的点权,但是话又说回来,我们可以注意到点权很小,于是考虑拆点。拆点后我们对于原来的边 \((i,j)\) 考虑拆分后的图长什么样?我们可以对于 \(\forall k,l,(i_k,j_l)\) 连边,并且边权和原来 \((i,j)\) 的相等即可。注意在后面计算的时候对于 \((i_k,j_l)\) 就只需要计算一次,注意不要算重。

建好图后我们考虑如何去计算答案。我们设 \(f_{i,,j}\) 表示对于左部点考虑到原图第 \(i\) 个点,在新图中右边还没匹配的个数为 \(j\) 时的最小答案。这个状态直接爆搜即可,时间复杂度 \(\mathcal O(n\times5^{2n})\)。爆搜的时候记录当前枚举到哪个点以及左右点的选取情况之类的信息即可。

int n, m, a[10], b[10], c[10][10], f[10][N], tot[10];
void dfs(int cur, int x, int y, int cntl, int cntr, int val){
    if(cur == m + 1){if(cntl == a[x])Min(f[x][cntr], f[x - 1][y] + val); return;}
    if(f[x - 1][y] == inf)return;
    for(int i = 0; i < 5; ++i){
        if(cntl + i > a[x] or i + y / tot[cur - 1] % (b[cur] + 1) > b[cur])break;
        dfs(cur + 1, x, y, cntl + i, cntr + i * tot[cur - 1], val + c[x][cur] * (i > 0));
    }
}
 
const string FileName = "";
signed main(){
    // fileio(FileName);
    n = rd(), m = rd();
    for(int i = 1; i <= n; ++i)a[i] = rd();
    for(int i = 1; i <= m; ++i)b[i] = rd();
    for(int i = 1; i <= n; ++i)for(int j = 1; j <= m; ++j)c[i][j] = rd();
    for(int i = 0; i <= n; ++i)for(int j = 0; j < N; ++j)f[i][j] = inf;
    f[0][0] = 0; tot[0] = 1; for(int i = 1; i <= m; ++i)tot[i] = tot[i - 1] * (b[i] + 1);
    for(int i = 1; i <= n; ++i)for(int j = 0; j < tot[m]; ++j)dfs(1, i, j, 0, j, 0);
    int res = inf; for(int i = 0; i < tot[m]; ++i)Min(res, f[n][i]);
    return wt(res == inf ? - 1 : res), 0;
}

CF1009F

长剖或者 dsu 都可以喵(秒)。

spxmcq

首先我们可以有一个果的 dp 式子:\(f_u=\sum\max(f_v,0)\),但是变成动态后就不好做了。

但是对于这种题我们一般考虑离线变成往图中加边维护连通块的问题。我们可以把贡献拆开算:

\[f_u=sz_u\times val+sum_u \]

我们只需要考虑一条边在何时会存在?可以看出当 \(f_u\ge0\) 时,也就是 \(val\ge{-sum_u\over sz_u}\) 时可以加入这条边。我们就用堆维护加边,用两棵 bit 分别维护 sz 和 sum 即可。

signed main(){
    // fileio(FileName);
    n = rd(); q = rd();
    for(int i = 2; i <= n; ++i)g[fa[i] = rd()].push_back(i);
    dfs(1); for(int i = 1; i <= n; ++i)ff[i] = i, a[i] = rd();
    for(int i = 1, u, x; i <= q; ++i)u = rd(), x = rd(), qs[i] = {x, u, i};
    sort(qs + 1, qs + 1 + q); int bs = qs[1].x; for(int i = 1; i <= q; ++i)qs[i].x -= bs;
    for(int i = 1; i <= n; ++i)a[i] += bs, qq.push(make_pair(- a[i], i));
    for(int i = 1; i <= n; ++i)sz.upd(lp[i], 1), sz.upd(lp[fa[i]], - 1), val.upd(lp[i], a[i]), val.upd(lp[fa[i]], - a[i]);
    for(int i = 1; i <= q; ++i){
        while(! qq.empty() and qq.top().first <= qs[i].x){
            int u = qq.top().second; qq.pop(); if(fd(u) != u or ! fa[u])continue;
            ff[u] = fd(fa[u]); int x = fd(u);
            sz.upd(lp[fa[u]], sz.ask(lp[u], rp[u])); val.upd(lp[fa[u]], val.ask(lp[u], rp[u]));
            if(fa[x])sz.upd(lp[fa[x]], - sz.ask(lp[u], rp[u])); val.upd(lp[fa[x]], - val.ask(lp[u], rp[u]));
            qq.push(make_pair(ceil(- val.ask(lp[x], rp[x]) * 1.0 / sz.ask(lp[x], rp[x])), x));
        }
        ans[qs[i].id] = sz.ask(lp[qs[i].u], rp[qs[i].u]) * qs[i].x + val.ask(lp[qs[i].u], rp[qs[i].u]);
    }
    for(int i = 1; i <= q; ++i)wt(ans[i]), pc('\n');
    return 0;
}

迷失游乐园

简单概率题。主要注意基环树的情况向上走的时候要先去处理环上的答案。我们可以先做好所有的向下走的期望长度,然后把环上的点拿出来暴力跑,因为环大小只有常数大小,于是 \(O(m^2)\) 求环上互相走的期望,其中 \(m\) 是环大小。注意实现细节,这里放一下代码。

int n, m, fa[N], son[N], hd[N], cnt;
db f[N], g[N], las[N], nxt[N];
struct edge{int nxt, to; db w;}e[N << 1];
inline void add(int u, int v, db w){e[++cnt] = {hd[u], v, w}; hd[u] = cnt;}
int huan[N], id[N], tot, rt;
bool O, cir[N];

void fdcir(int u, int Fa){
    cir[u] = true;
    for(int i = hd[u], v; i; i = e[i].nxt)if((v = e[i].to) != Fa){
        if(cir[v])return rt = v, void(); fdcir(v, u);
        if(! O and rt){if(rt == u)O = true; return;}
        if(O)break;
    }
    cir[u] = false;
}

void getcir(int u, int Fa){
    id[u] = ++tot; huan[tot] = u; fa[u] = 2;
    for(int i = hd[u], v; i; i = e[i].nxt)if((v = e[i].to) != Fa and cir[v]){
        if(! id[v])getcir(v, u); las[id[v]] = nxt[id[u]] = e[i].w; break;
    }
}

void dfs1(int u, int Fa){
    for(int i = hd[u], v; i; i = e[i].nxt)if((v = e[i].to) != Fa and ! cir[v])
        dfs1(v, u), ++son[u], fa[v] = 1, f[u] += f[v] + e[i].w;
    if(son[u])f[u] /= son[u];
}
void dfs2(int u, int Fa, db w){
    g[u] = w; if(fa[Fa] + son[Fa] > 1)g[u] += (g[Fa] * fa[Fa] + f[Fa] * son[Fa] - f[u] - w) / (fa[Fa] + son[Fa] - 1);
    for(int i = hd[u], v; i; i = e[i].nxt)if((v = e[i].to) != Fa)dfs2(v, u, e[i].w);
}

inline int pos(int x){return (x - 1 + tot) % tot + 1;}
void sol(){dfs1(1, 0); for(int i = hd[1]; i; i = e[i].nxt)dfs2(e[i].to, 1, e[i].w);}

void spc(){
    fdcir(1, 0); getcir(rt, 0);
    for(int i = 1; i <= tot; ++i)dfs1(huan[i], 0);
    for(int i = 1; i <= tot; ++i){
        int u = huan[i]; db p = 0.5;
        for(int j = pos(i + 1), v = huan[j]; j != i; v = huan[j = pos(j + 1)]){
            if(pos(j + 1) == i)g[u] += (las[j] + f[v]) * p;
            else g[u] += (f[v] * son[v] / (son[v] + 1) + las[j]) * p;
            p /= son[v] + 1;
        } p = 0.5;
        for(int j = pos(i - 1), v = huan[j]; j != i; v = huan[j = pos(j - 1)]){
            if(pos(j - 1) == i)g[u] += (nxt[j] + f[v]) * p;
            else g[u] += (f[v] * son[v] / (son[v] + 1) + nxt[j]) * p;
            p /= son[v] + 1;
        }
        for(int j = hd[u], v; j; j = e[j].nxt)if(! cir[v = e[j].to])dfs2(v, u, e[j].w);
    }
}

signed main(){
    ios :: sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    cin >> n >> m; for(int i = 1, u, v, w; i <= m; ++i)cin >> u >> v >> w, add(u, v, w), add(v, u, w);
    if(n != m)sol(); else spc(); db res = 0;
    for(int i = 1; i <= n; ++i)res += (f[i] * son[i] + g[i] * fa[i]) / (fa[i] + son[i]);
    cout << fixed << setprecision (5); cout << res / n; return 0;
}

Innophone

显然我们需要把贡献分成两部分。考虑右边是好做的,主要是维护左上角的贡献。我们可以扫描线做,现在需要维护 \(i\times y_i\) 的最大值,考虑 KTT 板子即可。下面大概讲一下 KTT。

其实 KTT 是一个比较暴力的东西,它的建树其实是一样的,但是因为我们的 \(i\times\) 是固定的所以相当于 KTT 去提前预判一个点在哪里有用,到什么时候就需要丢掉。所以我们只需要在正常线段树里面多维护时间标记。时间标记记录还有多久这个地方需要改变,有 \(t_u={\Delta y\over\Delta pos}\),然后在改的时候就多去看一个时间标记的限制即可。

inline void upd(int x){
    if(ma[ls] >= ma[rs])ma[x] = ma[ls], t[x] = (ma[ls] - ma[rs]) / (pos[rs] - pos[ls]), pos[x] = pos[ls];
    else ma[x] = ma[rs], t[x] = INF, pos[x] = pos[rs]; mint[x] = min({t[x], mint[ls], mint[rs]});
}
inline void add(int x, ll y){ma[x] += pos[x] * y; tg[x] += y; t[x] -= y; mint[x] -= y;}
inline void pd(int x){if(tg[x])add(ls, tg[x]), add(rs, tg[x]); tg[x] = 0;}
inline void bd(int x, int l, int r){
    if(l == r)return pos[x] = b[l], t[x] = mint[x] = INF, void();
    int mid = l + r >> 1; bd(lson); bd(rson); upd(x);
}
inline void mdf(int x, int l, int r, int ql, int qr, ll y){
    if(ql <= l and r <= qr and y <= mint[x])return add(x, y); int mid = l + r >> 1; pd(x);
    if(ql <= mid)mdf(lson, ql, qr, y); if(mid < qr)mdf(rson, ql, qr, y); upd(x);
}

signed main(){
    n = rd();
    for(int i = 1, x, y; i <= n; ++i)b[++m] = x = rd(), b[++m] = y = rd(), a[i] = {x, y};
    sort(b + 1, b + 1 + m); m = unique(b + 1, b + 1 + m) - b - 1;
    for(int i = 1; i <= n; ++i){
        int x = lower_bound(b + 1, b + 1 + m, a[i].x) - b;
        int y = lower_bound(b + 1, b + 1 + m, a[i].y) - b;
        ++d[1]; --d[x + 1]; ms[x].push_back(y);
    }
    for(int i = 1; i <= m; ++i)d[i] += d[i - 1]; bd(1, 1, m);
    for(int i = 1; i <= m; ++i){
        Max(ans, 1ll * b[i] * d[i] + ma[1]);
        for(int j : ms[i])mdf(1, 1, m, 1, j, 1);
    }
    return wt(max(ans, ma[1])), 0;
}

SUP-Supercomputer

首先贪心一定不劣,因为前面若干层点数小于等于 \(k\) 就直接干掉一整层,否则就每次删 \(k\) 个。现在考虑证明做法的存在性。设 \(c_i\) 表示深度大于 \(i\) 的点个数,于是有 \(ans=\max(i+\left\lceil c_i\over k\right\rceil)\),设转折深度为 \(h\)

我们先考虑倒过来做,于是就变成删掉若干叶子。叶子数单调不增,若叶子数小于等于 \(k\) 则说明每次已经可以删一层。否则一定是去除前 \(k\) 深的叶子删掉,我们考虑答案的变化。假设最浅的叶子深度大于 \(h\) 那么答案减一,没有问题。否则就矛盾了,于是得证。因为题目要求每个 \(k\) 的答案,于是我们小推一下式子:

\[\begin{aligned} ans&=i+\left\lceil c_i\over k\right\rceil\\ ans\times k&=ik+c_i\\ (ans-i)k&=c_i \end{aligned} \]

于是去维护凸包即可,对询问排序后就可以单调队列维护了。

inline db k(int x, int y){return x == y ? inf : (cnt[y + 1] - cnt[x + 1]) * 1.0 / (x - y);}

const string FileName = "";
signed main(){
    // fileio(FileName);
    n = rd(), m = rd(); for(int i = 1, x; i <= m; ++i)x = rd(), qs[i] = make_pair(x, i);
    dep[1] = cnt[1] = 1; sort(qs + 1, qs + 1 + m);
    for(int i = 2; i <= n; ++i)Max(maxdep, dep[i] = dep[rd()] + 1), ++cnt[dep[i]];
    for(int i = maxdep; i; --i)cnt[i] += cnt[i + 1];
    for(int i = 1; i <= maxdep; ++i){while(l < r and k(q[r], q[r - 1]) >= k(i, q[r]))--r; q[++r] = i;}
    for(int i = 1; i <= m; ++i){
        while(l < r and k(q[l], q[l + 1]) < qs[i].first)++l;
        ans[qs[i].second] = q[l] + (cnt[q[l] + 1] + qs[i].first - 1) / qs[i].first;
    }
    for(int i = 1; i <= m; ++i)wt(ans[i]), pc(' ');
    return 0;
}

动态 DP(加强版)

考虑 DDP 该怎么做怎么做,这就是个板。普通版就直接树剖线段树喵了,加强版虽然可以卡常过但是太不优美了。对于树上一只 \(\log\) 我们可以考虑 LCT,但是写 LCT 貌似太麻烦了,因为树的形态是固定的于是我们直接建立二叉搜索树。我们可以先跑树剖处理出每条重链,然后每次先对重链维护然后再递归到轻儿子,因为轻儿子又是另外的“重链”。其他和普通做法一样即可。

这里只放维护树的代码。

inline int bdl(int ql, int qr){
    int l = ql, r = qr, rt;
    while(l + 1 < r){int mid = l + r >> 1; if(chk(ql, mid, qr))l = mid; else r = mid;}
    t[rt = id[l]] = tmp[rt];
    if(ql < l)ls[rt] = bdl(ql, l), Fa[ls[rt]] = rt, t[rt] = t[ls[rt]] * t[rt];
    if(l + 1 < qr)rs[rt] = bdl(l + 1, qr), Fa[rs[rt]] = rt, t[rt] = t[rt] * t[rs[rt]];
    return rt;
}
inline int bd(int x){
    int u = x;
    for(; u; u = son[u]){
        tmp[u][0][0] = tmp[u][0][1] = g[u][0];
        tmp[u][1][0] = g[u][1]; tmp[u][1][1] = - inf;
        for(int v : e[u])if(v != son[u] and v != fa[u])Fa[bd(v)] = u;
    }
    for(; x; x = son[x])id[u++] = x, szz[u] = szz[u - 1] + sz[x] - sz[son[x]];
    return bdl(0, u);
}
inline void mdf(int u, int val){
    g[u][1] += val - a[u]; a[u] = val;
    tmp[u][1][0] = g[u][1];
    for(; u; u = Fa[u]){
        mat res = t[u]; t[u] = tmp[u];
        if(ls[u])t[u] = t[ls[u]] * t[u];
        if(rs[u])t[u] = t[u] * t[rs[u]];
        if(root(u)){
            int ff = Fa[u];
            tmp[ff][0][0] = tmp[ff][0][1] = g[ff][0] += max(t[u][0][0], t[u][1][0]) - max(res[0][0], res[1][0]);
            tmp[ff][1][0] = g[ff][1] += t[u][0][0] - res[0][0];
        }
    }
}

麻将

经典 dp of dp 题。对于这种题我们考虑第一次 dp 找到合法状态,第二次在合法状态上进行 dp。

参考了矩阵群的题解

这道题我们首先有 \(f_{i,j,k,0/1}\) 表示已经凑出 \(i\) 个面子,有 \(j\) 个两个连着的,当前有 \(k\) 个单牌,以及是否定好了对子。显然同一种顺子有三个可以看成同一种刻子有三个,因此一定可以使 \(j,k<3\),因为一般情况总共四个面子,所以有 \(i+j+k<5\),得出这样的状态一共有 54 种。然后就直接枚举每一维爆搜状态即可。还要注意 7 对子的特殊情况,这时我们只需要一个 \(f_i\) 记录对子的数量即可。

处理出了所有的状态我们就可以用这些状态 dp 了,设 \(f_{i,j,k}\) 表示前 \(i\) 种牌摸了 \(j\) 张走到状态 \(k\) 的方案数。转移只需要乘一个组合数即可。最后统计答案就枚举摸牌数以及合法状态即可。实际写的时候可以将第一维滚掉。时间复杂度 \(\mathcal O(n^2S)\)

int n, stacnt, f[N][M], g[N][M], ans;
int C[N][N], sta[5][M], cnt[105];
int typ[M], tot[3][M], dui[M], scnt, s[2][9][5][5][2];//s[typ][xxx/xx][2lian][dan][dui?]
ll ed[M];
unordered_map < ll , int > mp;
bitset < M > ok;

inline ll calc(ll st, int x){
    ll res = 0;
    for(int b = 0; b < scnt; ++b)if(st >> b & 1)if(typ[b])if(x == 2){if(tot[0][b] != 7)res |= 1ll << b + 1;}else{if(! x)res |= 1ll << b;}
    else{
        int i = tot[0][b], j = tot[1][b], k = tot[2][b], o = dui[b]; if(x < j + k)continue;
        int dt = x - j - k; i += j + dt / 3; j = k; k = dt % 3;
        if(~ s[0][i][j][k][o])res |= 1ll << s[0][i][j][k][o];
        if(! o and dt > 1)if(dt -= 2; ~ s[0][tot[0][b] + tot[1][b]][tot[2][b]][dt][1])res |= 1ll << s[0][tot[0][b] + tot[1][b]][tot[2][b]][dt][1];
    }
    return res;
}
void dfs(ll x){
    if(mp[x])return; mp[x] = ++stacnt; ed[stacnt] = x;
    for(ll i = 0, s = 0; i < 5; ++i)dfs(s |= calc(x, i));
}

void init(){
    C[0][0] = C[1][0] = 1; for(int i = 1; i < N; C[++i][0] = 1)for(int j = 1; j <= i; ++j)C[i][j] = Add(C[i - 1][j], C[i - 1][j - 1]);
    memset(s, - 1, sizeof s);
    for(int i = 0; i < 5; ++i)for(int j = 0; j < 3; ++j)for(int k = 0; k < 3; ++k)for(int o = 0; o < 2; ++o)if(i + j + k < 5)
        tot[0][scnt] = i, tot[1][scnt] = j, tot[2][scnt] = k, dui[scnt] = o, s[0][i][j][k][o] = scnt++;
    for(int i = 0; i < 8; ++i)typ[scnt] = 1, tot[0][scnt] = i, s[1][i][0][0][0] = scnt++;
    dfs(1ll << s[0][0][0][0][0] | (1ll << s[1][0][0][0][0]));
    for(int j = 1; j <= stacnt; ++j)for(ll i = 0, s = 0; i < 5; ++i)sta[i][j] = mp[s |= calc(ed[j], i)];
    for(int i = 1; i <= stacnt; ++i)ok[i] = (ed[i] >> s[0][4][0][0][1] & 1) | (ed[i] >> s[1][7][0][0][0] & 1);
    // cout << stacnt << endl;
    // for(int i = 1; i <= stacnt; ++i, pc('\n'))for(int j = 0; j < 5; ++j)wt(sta[j][i]), pc(' ');
}

const string FileName = "tst";
signed main(){
    // fileio(FileName);
    init(); n = rd(); for(int i = 1, x; i <= 13; ++i)++cnt[rd()], x = rd();
    f[0][1] = 1;
    for(int i = 1; i <= n; ++i, memset(g, 0, sizeof g)){
        for(int j = 0; j < i * 4 - 3; ++j)for(int k = 1; k <= stacnt; ++k)if(f[j][k])
            for(int q = cnt[i]; q < 5; ++q)ADD(g[j + q][sta[q][k]], Mul(f[j][k], C[4 - cnt[i]][4 - q]));
        for(int j = 0; j <= i * 4; ++j)for(int k = 1; k <= stacnt; ++k)f[j][k] = g[j][k];
    }
    for(int i = 14; i <= n * 4; ++i){
        int res = 0; for(int j = 1; j <= stacnt; ++j)if(! ok[j])ADD(res, f[i][j]);
        ADD(ans, Mul(res, Inv(C[n * 4 - 13][i - 13])));
    }
    return wt(ans), 0;
}
posted @ 2025-07-15 21:06  Lyrella  阅读(14)  评论(0)    收藏  举报