In mathematics you don't understand things, you just get used to them.

标签 greedy 下的文章

Link.

A. 赢钱 (money)

人类智慧题, 好像是写成二进制小数, 每次用 Least Significant Bit 去赌? 不是很能理解...

B. 排列 (per)

贡献可以分成五类: 环 (长度 $> 1$); 自环; 链 (长度 $> 2$); 链 (长度 $= 2$); 点. 记问题的答案为 $ans$, 一类贡献即将 $ans$ 乘 $2$, 二类贡献对 $ans$ 没有影响, 这两类都可以最后来计算, 先不管. 分别记三 / 四 / 五类贡献的数量为 $a$ / $b$ / $c$, 接下来进行分析.

这总共 $a+b+c$ 个元素对答案贡献是: 任意分组, 将组内元素拼成环的方案数之和之积. 但注意, 三类贡献由于自身可以反转, 因此要 $\times 2$, 这部分也放到最后来考虑, 而五类贡献不需要, 四类贡献最特殊, 它在自成一组的情况下不需要 $\times 2$, 否则需要. 考虑二项式反演, 设 $g_i$ 为至少有 $i$ 个四类元素自成一组的答案 (不考虑四类贡献的 $\times 2$ 1), $f_i$ 为恰好 (考虑四类贡献的 $\times 2$). 则由二项式反演, 有:

$$ f_i = 2^{b-i}\sum_{j=i}^b (-1)^{j-i}\binom ji g_j $$

考虑 $g_i$ 的求法, 有:

$$ g_i = \binom bi (a+b+c-i)! $$

为什么后面那部分是阶乘? 考虑已经加入了 $1 \sim i-1$, 现在加入 $i$, 它只能接在某个元素的后面, 或者自成一组, 一共 $i$ 种方案, 乘起来就是阶乘.

则:

$$ \begin{aligned} &\sum f_i \\ =&~2^{b-i}\sum_{i=0}^b\sum_{j=i}^b(-1)^{j-i}\binom ji g_j \\ =&~2^b\sum_{j=0}^bg_j\sum_{i=0}^j\left(\frac 12\right)^i(-1)^{j-i}\binom ji \\ =&~2^b\sum_{j=0}^bg_j \left(-\frac 12\right)^j \end{aligned} $$

直接计算即可. 说起来比较麻烦, 其实还是比较简单的, 应该是真正的签到题.

C. 箱子 (box)

贪心策略就是选择极长区间操作, 证明可以考虑贡献形式是求和, 因此操作区间不会相交, 只会包含或无交. 于是用线段树维护左右端点的信息, 区间的答案, 区间如果全部同色的答案 (为了区间覆盖操作) 即可.

D. 排列 (permutation)

学了下高一同学的模拟退火.


/ When thy inconsiderate hand /

/ Flings ope this casement, with my trembling name, /

/ To look on one, whose wit or land, /

/ New battery to thy heart may frame, /

/ Then think this name alive, and that thou thus /

/ In it offend’st my Genius./

—— [John Donne - A Valediction of My Name in the Window]()


  1. 为什么不考虑? 如果在 $g_i$ 的阶段就计算了, 后面就无法套用二项式定理优化复杂度了.

link。

首先二分答案固定每个 bot 的步长,然后就基本上弱于 codeforces - 1476F 了,但是还是有些不一样的地方。

假如我们是在一个序列上做 dp,不妨把原环按 $n$-$1$ 之间的间隙破开。令 $f(i)$ 表示经过决策前 $i$ 个 bots,最远能覆盖到的坐标(连续地覆盖了 $1 \sim f(i)$)。转移即分类讨论,同时令 $x$ 为当前步数:

  • 前 $i-1$ 个 bots 能够覆盖到 $i-1$,那么 bot $i$ 就可以往后面倒,即 $f(i) \gets p_i+x$;$(1)$
  • 前 $i-1$ 个 bots 不能够覆盖到 $i-1$,但是可以覆盖到 $i-x-1$,那么 bot $i$ 就只能往前面倒来把序列连上。$(2)$

乍看这样的分类讨论已经足够了,但是这个 dp 状态实际上是省略了一维的,即 $k$,$k$ 表示在 $1 \sim i$ 的 bots 当中是 bot $k$ 来将 $1 \sim f(i-1)$ 的殖民地连上的,基于贪心的策略我们把 $k$ 省掉,即尽量取靠后的 $k$。上述论证是有错误的,但大致上的意思没错,即转移如果最朴素地做并不是连续的。再看到我们的转移,如果 bot $i$ 连上了 $1 \sim f(i-2)$,则 bot $i-1$ 可以往后倒从而使 $f(i) \gets p_{i-1}+x$。$(3)$

考虑环。只需要分类讨论 bot $1$ 往左往右倒即可,比较容易。

/*-ukpkmpkk-*/
#include <iostream>
#include <algorithm>
#define cmax(x, y) x = max(x, y)
using std::cin; using std::cout;
using std::min; using std::max;
int m, n, a[200100], p[100100], dp[100100];
bool chk(int cur) {
    dp[1] = 0;
    for (int i=2; i<=n; ++i) {
        dp[i] = dp[i-1];
        if (dp[i-1] >= p[i]-1) {
            cmax(dp[i], p[i]+cur);
        }
        if (dp[i-1] >= p[i]-cur-1) {
            cmax(dp[i], p[i]);
        }
        if (i > 2 && dp[i-2] >= p[i]-cur-1) {
            cmax(dp[i], p[i-1]+cur);
        }
    }
    if (dp[n] >= m-cur-1) return 1;
    dp[2] = max(p[2], cur);
    for (int i=3; i<=n; ++i) {
        dp[i] = dp[i-1];
        if (dp[i-1] >= p[i]-1) {
            cmax(dp[i], p[i]+cur);
        }
        if (dp[i-1] >= p[i]-cur-1) {
            cmax(dp[i], p[i]);
        }
        if (dp[i-2] >= p[i]-cur-1) {
            cmax(dp[i], p[i-1]+cur);
        }
    }
    if (dp[n] >= min(m-1, m+p[2]-cur-1)) return 1;
    return 0;
}
signed main() {
    std::ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> m >> n;
    for (int i=1; i<=n; ++i) {
        cin >> a[i];
        a[i+n] = a[i]+m;
    }
    std::sort(a+1, a+n+n+1);
    int pos = 1;
    for (int i=2; i<=n; ++i) {
        if (a[i+1]-a[i] > a[pos+1]-a[pos]) {
            pos = i;
        }
    }
    for (int i=1; i<=n; ++i) {
        p[i] = a[i+pos];
    }
    for (int i=2; i<=n; ++i) {
        p[i] -= p[1];
    }
    p[1] = 0;
    int l = 0, r = a[pos+1]-a[pos]-1, mid, res = r;
    while (l <= r) {
        if (chk(mid = (l+r)/2)) r = mid-1, res = mid;
        else l = mid+1;
    }
    cout << res << "\n";
}

「codeforces - 1592F2」Alice and Recoloring 2:妙妙题,显然只会操作 $(1, 1)$ 和 $(n, m)$,分别记作操作 1 和操作 2。我们希望单点操作,所以考虑差分(trick),只是这个题的差分非常高妙。如果你直接对前缀做二维差分会发现操作 1 要修改四个点,操作 2 修改一个点,而操作 1 花费 $1$,2 花费 $2$,所以每个点可能被操作最多两次(考虑一种情况,即 $(1, 1)$,$(1, 2)$,$(2, 1)$ 是黑点,$(2, 2)$ 是白点)。xf 教我了一种高庙的差分,具体是 $c_{i, j} = a_{i,j} \oplus a_{i+1, j} \oplus a_{i, j+1} \oplus a_{i+1, j+1}$,这样差分操作 2 就修改四个点,操作 1 修改一个点,这样一个点就最多修改一次了,建出二分图即可。

最后的做法是,假设我全部用操作 1,然后反悔看最多能用多少次操作 2(因为会用在 $(i, j)$ 上用操作 2 的情况是 $(i, j)$,$(n, j)$,$(i, m)$ 都是黑点)。

最后 $(n, m)$ 是需要特判的。

各种意义上我不会的题,膜拜 xf。

char s[600];
int n, m, a[600][600];
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m;
    for (int i=1; i<=n; ++i) {
        cin >> s;
        for (int j=1; j<=m; ++j) {
            a[i][j] = s[j-1] == 'B';
        }
    }
    for (int i=1; i<=n; ++i) {
        for (int j=1; j<=m; ++j) {
            a[i][j] ^= a[i+1][j]^a[i][j+1]^a[i+1][j+1];
        }
    }
    mf_graph<int> g(n+m+2);
    const int src = n+m+1, ter = n+m+2;
    for (int i=1; i<n; ++i) {
        for (int j=1; j<m; ++j) {
            if (a[i][j] && a[n][j] && a[i][m]) {
                g.add_edge(i, j+n, 1);
            }
        }
    }
    for (int i=1; i<=n; ++i) g.add_edge(src, i, 1);
    for (int i=1; i<=m; ++i) g.add_edge(i+n, ter, 1);
    int ret = -g.flow(src, ter);
    a[n][m] ^= (-ret)&1;
    for (int i=1; i<=n; ++i) {
        for (int j=1; j<=m; ++j) ret += a[i][j];
    }
    cout << ret << "\n";
}

「codeforces - 1146G」Zoning Restrictions:还行的题。想到把每个点拆成 $h+1$ 个点,如果你定义状态 $(i, j)$ 表示第 $i$ 个点的高度为 $j$ 会发现并不优秀,改成高度大于等于 $j$ 会好一点。关于这题怎么想到 min-cut 的,可以这样考虑(which is also a common trick):全局最理想的答案是 $h^2 \times n$,然而不一定合法,我们以最小的代价去把这个答案调整到合法,得到的一定是最优的答案,因此是 min-cut。然而这道题我们不会这样去想,这只是一些题的思路。对于这个题我们的想法是把所有的贡献取负,这样就成了最小值()

这里涉及另一个思维上的 trick:最小割的源汇有什么意义?划分出的两个点集有什么意义?在这一题中我们不妨将点集 $S$ 中的事件称为「成立」,$T$ 中的事件称为「不成立」,这样就赋予了最小割意义。

这里给出构造网络的方案:$\lang(i, j), (i, j+1), -j^2\rang$,$\lang (j, 0), x_i+1, \infty \rang, \forall j \in [l_i, r_i]$,$\lang i, t, c_i \rang$。

考察意义:割掉第一类边的意义即令房子 $i$ 的高度为 $j$,这对答案的贡献为 $j^2$,我们对答案取了负,所以贡献为 $-j^2$,如果第二类边有流量则意味着付罚金,因为一个区间只需要付一次罚金,所以我们不能直接割掉第二类边,而是通过第二类边使得割掉第三类边有意义(断掉连通性)。

当然流量不可能是负的,所以选择给 $-j^2$ 加上 $h^2$,因为 $n$ 幢房子代表的每一条链必然割且仅割一条一类边,所以最后给答案加上 $h^2 \times n$ 即可。

const int inf = 0x3f3f3f3f;
int n, h, m, id[60][60], qwq;
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> h >> m;
    for (int i=1; i<=n; ++i) {
        for (int j=0; j<=h+1; ++j) id[i][j] = ++qwq;
    }
    mf_graph<int> g(qwq+m+2);
    const int src = qwq+m+1, ter = qwq+m+2;
    for (int i=1; i<=n; ++i) {
        g.add_edge(src, id[i][0], inf);
    }
    for (int i=1; i<=n; ++i) {
        for (int j=0; j<=h; ++j) g.add_edge(id[i][j], id[i][j+1], h*h-j*j);
    }
    for (int i=1,l,r,x,c; i<=m; ++i) {
        cin >> l >> r >> x >> c;
        g.add_edge(i+qwq, ter, c);
        for (int j=l; j<=r; ++j) {
            g.add_edge(id[j][x+1], i+qwq, inf);
        }
    }
    cout << h*h*n-g.flow(src, ter) << "\n";
}

「codeforces - 1061E」Politics:第一步是想到把 tree 1 放左部 tree 2 放右部。然后进入这个题的 key step:将对于子树的限制(被钦定的结点个数)转化为对一个结点的限制。这里大部分的做法是:对于一个有限制的结点 $x$,将 $k_x$ 减去 $\displaystyle \sum_{y \in \text{subtree}(x), y \neq x} k_y$,从而我们的工作就变成了在「所有 $x$ 的子树中的有限制的结点的上方中钦定一些结点」。于是给出这题的建图(费用流):$\displaystyle \lang s, x \in\textbf V_1,k_x-\sum_{y \in \text{subtree}(x), y \neq x} k_y, 0\rang$,$\displaystyle \lang x \in\textbf V_2, t,k_x-\sum_{y \in \text{subtree}(x), y \neq x} k_y, 0\rang$,$\lang up_i, up'_i, 1, a_i\rang$,其中 v_1 v_2 是两棵树的点集,up_i 是编号 $i$ 在 tree 1 中对应的结点的最近有限制祖先,up'_i 同理。

理解一下,一、二类边都对应每个结点的限制,第三类边就是反过来考虑(我们之前是对祖先 $x$ 考虑,要在子树中所有有限制的结点的上面钦定结点),现在我们对一个点 $x$ 考虑它对最近有限制祖先的贡献。如果钦定了编号 $i$,则会对答案造成 $a_i$ 的贡献,占据 up_i 和 up'_i 的空余可钦定数。

mcf_graph<int, int> g;
int src, ter, qwq, qaq;
int n, rtx, rty, q1, q2, a[600], bstx[600], bsty[600], upx[600], upy[600], parx[600], pary[600];
bsi<int> adjx[600], adjy[600];
int dfsx(int x, int pa, int t) {
    parx[x] = pa;
    if (bstx[x]) {
        t = x;
    }
    upx[x] = t;
    int sum = 0;
    for (auto y : adjx[x]) {
        if (y != pa) {
            sum += dfsx(y, x, t);
        }
    }
    if (bstx[x]) {
        if (bstx[x] < sum) fucked_up();
        qwq += bstx[x]-sum;
        g.add_edge(src, x, bstx[x]-sum, 0);
        return bstx[x];
    }
    return sum;
}
int dfsy(int x, int pa, int t) {
    pary[x] = pa;
    if (bsty[x]) {
        t = x;
    }
    upy[x] = t;
    int sum = 0;
    for (auto y : adjy[x]) {
        if (y != pa) {
            sum += dfsy(y, x, t);
        }
    }
    if (bsty[x]) {
        if (bsty[x] < sum) fucked_up();
        qaq += bsty[x]-sum;
        g.add_edge(x+n, ter, bsty[x]-sum, 0);
        return bsty[x];
    }
    return sum;
}
int getx(int x, int pa) {
    int res = 0;
    for (auto y : adjx[x]) {
        if (y != pa) res += getx(y, x);
    }
    if (bstx[x]) {
        return bstx[x];
    }
    return res;
}
int gety(int x, int pa) {
    int res = 0;
    for (auto y : adjy[x]) {
        if (y != pa) res += gety(y, x);
    }
    if (bsty[x]) {
        return bsty[x];
    }
    return res;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> rtx >> rty;
    for (int i=1; i<=n; ++i) {
        cin >> a[i];
    }
    for (int i=1,x,y; i<n; ++i) {
        cin >> x >> y;
        adjx[x] += y, adjx[y] += x;
    }
    for (int i=1,x,y; i<n; ++i) {
        cin >> x >> y;
        adjy[x] += y, adjy[y] += x;
    }
    cin >> q1;
    for (int i=0,x; i<q1; ++i) {
        cin >> x >> bstx[x];
    }
    cin >> q2;
    for (int i=0,x; i<q2; ++i) {
        cin >> x >> bsty[x];
    }
    g = mcf_graph<int, int>(n*2+2);
    src = n*2+1, ter = n*2+2;
    dfsx(rtx, 0, 0), dfsy(rty, 0, 0);
    if (qwq != qaq) fucked_up();
    for (int i=1; i<=n; ++i) {
        if (upx[i] && upy[i]) {
            g.add_edge(upx[i], upy[i]+n, 1, -a[i]);
        }
    }
    auto [X, Y] = g.flow(src, ter);
    if (X != qwq) fucked_up();
    cout << -Y << "\n";
}

link。

考虑把原问题写成一个在 $\left(\log_2 \max v \right) \times n$ 的矩阵里选出三列,我们首先预处理出 $j \cap q$。具体,我们需要对于每一个权值 $v$ 求出一个最大的下标 $p$($1 \leqslant p \leqslant n$)满足存在极大的 $q < p$ 且 $v \cap a_p \cap a_q = v$,即 $v \subseteq \left(a_p \cap a_q\right)$,这个可以做一个二元组 dp,即设 $f_v$ 为对于 $v$ 而言的答案,注意到 $p$ 和 $q$ 的实际意义是「满足左右两边存在有一个位置做并操作后是 $v$ 的超集的位置下标」的最大值和次大值,所以更新答案是容易的。

考虑如何转移。对于一个左闭右开区间 $[0, 2^n)$,我们分治求出 $[0, 2^{n-1})$ 和 $[2^{n-1}, 2^n)$ 的 dp,当然左边区间的 dp 值不会对右边区间产生影响,所以我们考虑右边区间对左边区间的影响。$\forall i \in [l, m)$,我们需要从 $i$ 的超集转移到 $i$,因为在 dp 时我们实际上是把所有贡献放到一个点上,又注意到 $i-l+m$ 和 $i$ 的关系就是二进制意义下多了一个最高位的 $1$,所以只需要从 $i-l+m$ 转移到 $i$ 即可(有点谜语,但就这样吧)。

然后就贪心取最高位,挨个取 max 就行了。

int n, a[1000100], qwq;
pii dp[3000100];
pii upd(const pii& x, const pii& y) {
    if (x.first > y.first) {
        return pii(x.first, max(y.first, x.second));
    }
    else {
        return pii(y.first, max(x.first, y.second));
    }
}
void sos_dp(int l, int r) {
    if (r-l == 1) {
        return;
    }
    int mid = (l+r)/2;
    sos_dp(l, mid), sos_dp(mid, r);
    for (int i=l; i<mid; ++i) {
        dp[i] = upd(dp[i], dp[i-l+mid]);
    }
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    for (int i=1; i<=n; ++i) {
        cin >> a[i];
        dp[a[i]] = upd(dp[a[i]], pii(i, 0));
    }
    qwq = 1;
    for (int up=*max_element(a+1, a+n+1); (1<<qwq) <= up;) {
        qwq++;
    }
    sos_dp(0, 1<<qwq);
    int ans = 0;
    for (int i=1; i<=n; ++i) {
        int offset = 0;
        bool f = 0;
        for (int j=qwq; j>=0; --j) {
            if (~(a[i]>>j)&1 && dp[offset|(1<<j)].second > i) {
                offset |= 1<<j, f = 1;
            }
        }
        if (dp[offset].second > i) {
            f = 1;
        }
        if (f) {
            cmax(ans, a[i]|offset);
        }
    }
    cout << ans << "\n";
}