随便写点(2)

本讲作业#

例题#

例1 【The Number Games】 CF-980E
例2 【Tree Shuffling】CF-1363E
例3 【删括号】 https://ac.nowcoder.com/acm/problem/21303
例4 【Company】 CF-1062E
例5 【Lightest language】 SP186
例6 【Tree】CF-468D (未考虑:字典序最小)
例7 【方差】DP讲解。

总结#

复盘【删括号】和【Tree shuffling】思维流程
复盘【Tree】 CF468D 每一步如何往下思考的。
【Lightest language】 复现正确性证明。

  • 思考另外一些贪心方法错误的原因(课上提到一个)。
  • 如果要求 树是满 k 叉树,应该如何解决。请给出 O(nlogk) 算法!

作业#

1【Big Bishops】 SGU-221 (区间DP 练习题)
2【圣诞树】 poj3013
3【黑白树】https://ac.nowcoder.com/acm/problem/13249
4【Teleporter】 agc004d
5【Royal Federation】SGU-216
6【宝藏 NOIP'17】 (状压dp)P3959
7 上次的【Ants in leaves】 需要自己完成证明。

例题#

CF980E#

trick:不会正攻时可以考虑反攻。

关键转化:注意到删点不好删,考虑选点。

首先,n 号节点是一定需要选择的。

然后观察到每个节点的权重是 2i,而 2i>j=1j<i2j。因此,我们考虑贪心的使得剩下的最大编号的点 x 能选就选。

可以用 树状数组 判断 x 是否能选。

/*******************************
| Author:  DE_aemmprty
| Problem: The Number Games
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/CF980E
| When:    2024-09-12 21:39:50
| 
| Memory:  250 MB
| Time:    3000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;
 
long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}
 
const int N = 1e6 + 7;
 
int n, k, dfn[N], siz[N], dep[N], tot, f[N];
vector <int> to[N];
bool vis[N];
 
namespace BIT {
    int tr[N], n;
    void init(int x) { n = x, fill(tr + 1, tr + n + 1, 0);}
    int lowbit(int x) { return x & (-x);}
    void update(int x, int y) {
        for (; x <= n; x += lowbit(x))
            tr[x] += y;
    }
    int query(int x) {
        int res = 0;
        for (; x; x -= lowbit(x))
            res += tr[x];
        return res;
    }
}
 
void dfs(int u, int fa, int d) {
    dfn[u] = ++ tot; f[u] = fa;
    siz[u] = 1, dep[u] = d;
    for (int v : to[u])
        if (v != fa) {
            dfs(v, u, d + 1);
            siz[u] += siz[v];
        }
}
 
void solve() {
    n = read(), k = read();
    for (int i = 1; i < n; i ++) {
        int a = read(), b = read();
        to[a].push_back(b);
        to[b].push_back(a);
    }
    dfs(n, 0, 1);
    BIT::init(n);
    vis[n] = 1; int cnt = 1;
    BIT::update(1, 1);
    for (int i = n - 1; i >= 1 && cnt < n - k; i --) {
        if (vis[i]) continue;
        if (dep[i] - BIT::query(dfn[i]) + cnt <= n - k) {
            int tmp = i;
            while (tmp && !vis[tmp]) {
                BIT::update(dfn[tmp], 1);
                BIT::update(dfn[tmp] + siz[tmp], -1);
                cnt ++, vis[tmp] = 1;
                int x = tmp;
                tmp = f[tmp];
                f[x] = 0;
            }
        }
    }
    for (int i = 1; i <= n; i ++)
        if (!vis[i]) cout << i << ' ';
}
 
signed main() {
    int t = 1;
    while (t --) solve();
    return 0;
}

CF1363E#

trick:无。

关键观察:显然,我们的策略一定是按照 ai 从小往大去操作。

考虑最小的 ai,那么我们肯定先操作这个点 i,使得 i 子树内尽量匹配。

那么以此类推,我们很容易发现只需要让 ai 从小往大贪心即可。

又容易发现 bi=ci 的点是充数的,没有任何作用。

那么我们可以对每个点 i,存储子树内 0110 的个数。然后就可以快速操作了。

写代码的一个细节:虽然说是 ai 从小往大选,但实际上你可以直接看 ai 是否是 i1 路径上所有 ai 的最小值来判断是否需要操作 i。证明显然易见。

/*******************************
| Author:  DE_aemmprty
| Problem: E. Tree Shuffling
| Contest: Codeforces - Codeforces Round 646 (Div. 2)
| URL:     https://codeforces.com/problemset/problem/1363/E
| When:    2024-09-23 23:08:58
| 
| Memory:  256 MB
| Time:    2000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;

long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}

const int N = 2e5 + 7;

int n;
long long a[N], b[N], c[N];
vector <int> to[N];
long long ans;
int cnt0[N], cnt1[N];

struct Node {
    int p, q;
    Node operator + (const Node &x) const {
        return (Node) {p + x.p, q + x.q};
    }
};

Node dfs(int u, int fa, long long mn) {
    cnt0[u] = (b[u] == 0 && c[u] == 1);
    cnt1[u] = (b[u] == 1 && c[u] == 0);
    Node del = {0, 0};
    for (int v : to[u]) {
        if (v == fa) continue;
        del = del + dfs(v, u, min(mn, a[u]));
        cnt0[u] += cnt0[v];
        cnt1[u] += cnt1[v];
    }
    if (mn > a[u]) {
        int T = min(cnt0[u], cnt1[u]);
        del = del + (Node) {T, T};
        cnt0[u] -= T, cnt1[u] -= T;
        ans += a[u] * T * 2;
    }
    return del;
}

void solve() {
    n = read();
    for (int i = 1; i <= n; i ++)
        a[i] = read(), b[i] = read(), c[i] = read();
    for (int i = 1, u, v; i < n; i ++) {
        u = read(), v = read();
        to[u].push_back(v);
        to[v].push_back(u);
    }
    Node res = dfs(1, 0, 2e18);
    if (cnt0[1] || cnt1[1]) {
        cout << -1 << '\n';
    } else {
        cout << ans << '\n';
    }
}

signed main() {
    int t = 1;
    while (t --) solve();
    return 0;
}

牛客 21303#

没账号,不做了/fn。

CF1062E#

trick:区间 LCA 实际上只是两个点的 LCA。

关键知识点:会区间 LCA

首先我们知道区间 LCA 是选择一些点中最左边的点和最右边的点的 LCA

然后最左边和最右边的点可以用欧拉序加上 st 表解决。

笑点解析:到这里你就可以通过 DSU On Tree 解决了。但这实在太蠢了。

现在,题目要求你删掉一个点。容易发现如果删掉的点不是计算 LCA 的两点,对最终答案是没有影响的。

维护一下区间欧拉序最大,次大,最小,次小即可。

SP186#

trick:遇到有关一个串是另一个串的前缀的题目时考虑字典树,从 naive 的贪心入手优化。

观察一:由于题目禁止一个字符串是另一个字符串的前缀,所以考虑字典树。

考虑 Trie。容易发现在题目条件的要求下,每个字符串的结尾一定是叶子节点。

转化题目:构造 n 个叶子节点的 k 叉树,使得所有叶子到根的边权之和最小。

关键转换:从上往下进行贪心,而不是从下往上。

发现从叶子一个个向上合并不太容易做,考虑从根向下去贪心。

容易发现一个非常 naive 的贪心:每次选择权值最小的一个叶子节点,然后给这个叶子节点加 k 个儿子。

这样很明显是错的。当一个字母的权值极大时,我们不如不选这种字母作为边,也就是删除这个边。

那什么贪心方法是正确的呢?

观察二:答案的字典树一定是一颗除叶子节点外塞满 k 个儿子的树删掉权值最大的一些叶子后得到的。

根据观察二,我们可以继续使用上面的贪心方法,但是在叶子节点达到 n 个之后还要继续贪心(这样可以删除一些权值极大的叶子节点,加入一些权值较小的叶子节点),直到扩展后不优时停止扩展。


考虑证明。

时间复杂度正确性#

假设现在你需要对在所有 p 个叶子节点内最优的树内进行删点。

你删除的点数 pnp×k2k

化简式子,得到 pn×k2

又由于 k26,所以 p13×n。因此这棵树不会很大,从而时间复杂度是正确的。

算法正确性#

由于这个算法的本质是对满儿子的 k 叉树进行删叶子节点,所以我们分两部分考虑。

Part. 1

如果现在我们必须对每个点选满儿子,那么考虑归纳。

假设现在所有叶子节点的权值排序后得到的序列 {a1,a2,,ap} 是最优的。

如果我们不选择 a1 进行更新,假设选择了 at(t>1)

容易发现有 at×k+i=1kbia1×k+i1kbi,显然不优。

Part. 2

由于每个点的本质相同,我们现在计算出了一个在所有 p 个叶子节点内最优的树之后,暴力删除权值前 q 大的叶子节点一定是最优的。


值得写代码。

/*******************************
| Author:  DE_aemmprty
| Problem: LITELANG - The lightest language
| Contest: Luogu
| URL:     https://www.luogu.com.cn/problem/SP186
| When:    2024-09-25 23:35:29
| 
| Memory:  1 MB
| Time:    5000 ms
*******************************/
#include <bits/stdc++.h>
using namespace std;

long long read() {
    char c = getchar();
    long long x = 0, p = 1;
    while ((c < '0' || c > '9') && c != '-') c = getchar();
    if (c == '-') p = -1, c = getchar();
    while (c >= '0' && c <= '9')
        x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
    return x * p;
}

const int N = 1e4 + 7;

int n, k;
long long a[N];
multiset <int> s;

void solve() {
    n = read(), k = read();
    long long ans = 2e18, res = 0, sum = 0;
    s.clear();
    for (int i = 1; i <= k; i ++) {
        a[i] = read();
        sum += a[i];
        s.insert(a[i]);
    }
    res = sum;
    while (true) {
        if (res >= ans) break;
        if ((int) s.size() == n)
            ans = res;
        auto it = s.begin();
        long long val = (*it);
        res += val * (k - 1) + sum;
        s.erase(it);
        for (int i = 1; i <= k; i ++)
            s.insert(val + a[i]);
        while ((int) s.size() > n) {
            res -= (*s.rbegin());
            s.erase(prev(s.end()));
        }
    }
    cout << ans << '\n';
}

signed main() {
    int t = read();
    while (t --) solve();
    return 0;
}

CF468D (未考虑:字典序最小)#

trick:将路径之和转化为每条边经过的次数乘上边权,将重心提到根上。

关键转化:发现 d(i,pi) 不太好算,转化为对于每一条边 w 乘上这条边被经过的次数。

第一步就被误导了。

我们发现 d(i,pi) 不太好算,于是将题目转化为对于每一条边 w 乘上这条边被经过的次数。这时候,显然每一条边被经过的次数 x2×min{w1,w2},其中 w1,w2 是这条边左右两边的连通块大小。

我们肯定是想把它取满的。但事实证明确实可以取满。以下是构造:

  • 我们把重心提到根上。容易发现,当重心提到根上时,较小的一块连通块一定是在下面的。(重心的性质)

  • 因此,我们可以把每个与根相邻的点的子树中的点连到外面的子树即可。

因此,答案就是上面这个式子。感觉很逆天。

作者:DE_aemmprty

出处:https://www.cnblogs.com/aemmprty/p/18428066

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   DE_aemmprty  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示