CSP-S 35

10.20

神秘%你赛,rk1 170 ,你管这叫CSP-S?

t1

赛时狂写t1 ,想了半天想出来个神秘做法,时间复杂度不会证但应该是对的,写完本地大阳历1.2s 感觉应该没啥大问题,结果空间炸了,最后2h写的代码和暴力分一样多 \(\ldots\)

正解:

其实正解代码特别好写,就是思路巧妙。

我们发现在线维护这个东西时间是会爆的(要不然就空间炸,反正我不会在线,真的有在线做法吗?),于是考虑离线。

发现对于每个点我们只需要求含有该点的连通块个数,而并不需要明确这些连通块是什么。

于是可以离线后倒推出答案(倒推下面会解释)。

\(cnt_i\) 表示含有点 \(i\) 的连通块个数,考虑如何合并。

合并显然是取并集,但关键在于如何不重

发现对于树上连通块合并,当且仅当两个连通块之间未进行过合并操作时,合并后的连通块中元素个数等于原两连通块个数之和

这一点是显然的,若两连通块未合并过,则取并后个数自然为原个数和;而若已合并过,则两连通块中一定有相同元素,故合并后的连通块中元素个数一定小于原两连通块个数之和。

现在我们开始考虑如何合并曾合并过的连通块

对于集合显然有:

\(|S\cup T|=|S|+|T|-|S\cap T|\)

则我们只需记录两个连通块上次合并后的大小即可。

现在连通块就已经合并完了。

但这并不是我们要求的\(\ldots\) 吗?

手模样例发现,合并时维护的“连通块大小”,其实就是答案。

继续思考,我们发现:

由于倒推,不会算重(具体的,若一条边是最后一次被操作,则倒推后化为第一次,所以相应的点的出现次数只有第一次才会被更新,以此类推)。

对于两个已未合并过的连通块,显然该连通块内所有点出现次数都要加 1 ,而我们只对两连通块之间的边直接连接的两个点累加了出现次数,对剩下的点进行了类似懒标记的处理方式,即再次合并到这些点时再累加出现次数。

对于两个已合并过的连通块,再次合并时,由于这两块已合并过,所以这两块公共的元素至少包含两连通块之间的边直接连接的两个点,而不同连通块合并后取并元素一致,所以这次合并后连通块的大小即为两点(上文提到过的)的出现次数(考虑连通块中元素是通过合并得来的,合并时对两边连通块的影响本质一样,因此当前连通块的大小是多少,该点也被合并的其它连通块中的此数也是多少(倒推后),因此当前连通块的大小即为该点出现此数)。

我感觉讲的还是太抽象了,还是建议自己画图手模样例。

code

哒哒哒
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 10;
int n, m;
pair<int, int> e[N];
int cnt[N], las[N], q[N];
bool flag[N];

signed main()
{
    freopen("set.in", "r", stdin);
    freopen("set.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1, u, v; i < n; ++i)
    {
        cnt[i] = 1;
        cin >> u >> v;
        e[i] = make_pair(u, v);
    }
    cnt[n] = 1;
    for (int i = 1; i <= m; ++i)
        cin >> q[i];
    int num = 0;
    for (int i = m; i; --i)
    {
        int u = e[q[i]].first, v = e[q[i]].second;
        if (!flag[q[i]])
        {
            num = cnt[u] + cnt[v];
            cnt[v] = cnt[u] = num;
            las[q[i]] = cnt[u];
        }
        else
        {
            num = cnt[u] + cnt[v] - las[q[i]];
            cnt[v] = cnt[u] = num;
            las[q[i]] = cnt[u];
        }
        flag[q[i]] = 1;
    }
    for (int i = 1; i <= n; ++i)
        cout << cnt[i] << ' ';
    return 0;
}

t2

赛时少判0+未取等 60 --> 10

其实少判0在赛时看出来了,但是上交时刚好结束👿

容易发现当 \(k=n-1\) 时是好处理的。

对于所有的 \(a_i > m\),永远不可能被拿完,所以删去这部分值,最终答案取 \(\max\) 即可。

对于剩下的序列,发现对于任意两种硬币所构成的区间长度一定为 \(m+1\) ,此时是满足条件的最优解。

则只需考虑让所有区间的开始位置尽量小即可。

所以我们将最小值放在开头,次小值放在结尾,中间按照降序填充元素(值越大越可能产生区间重叠,这显然是更优的,因此我们将两个小值放在两端减少其产生影响)。

题解的解释:

显然,这个区间的起点应该至少为 \((x + m − 1) − y + 1 + 1 = x + m + 1 − y\) 。我们肯定每次会尽可能往前放置区间。所以总长度应该是 \((\sum\limits_{i=2}^{n}m + 1 − a_i) + a_n\) ,也就是 \(m + 1+\sum\limits_{i=2}^{n-1}m + 1 − a_i\) ,为了最小化这个式子,我们显然可以将最小的两个数放在 1 和 n 就行。

对于 \(k=n-2\) 的情况,考虑将其差为两个 \(k=n-1\) 再将其合并。

正确性显然\(\ldots\)

感性理解一下\(\ldots\)

题解的证明:

考虑这样做的正确性。假如我们定好了一个合法方案中每个区间的覆盖范围。如果两个区间会被一个长为 \(m\) 的区间同时包含,我们就在这两个区间之间连一条边。显然图不可能出现大小大于等于 \(3\) 的环,否则就能找到区间同时包含这些区间。那么原图一定可以被二染色。也就是分成两个点集,每个点集内部没边。这样就一定对应了两个 \(k = n − 1\) 的问题。也就是说,上面的做法是充要的。

对于所有的 \(a_i > m\),进行同样操作。

然后首先去掉最小的四个元素(因为这四个元素一定在首或尾,与最终答案无关,这里注意特判元素个数小于等于 \(4\) 的情况),之后将剩下的元素分为两个集合,答案取最优即可。

分组的过程可看作 0/1 可行背包,bitset优化后有 80 甚至 90 pts ,(如果乱搞一下甚至AC)。

然而这并不是正解。

观察 \(k=n-1\) 时的式子,发现当去除掉冗余元素后,贡献可转化为 \(b_i=m+1-a_i\) ,最终答案即为 \(m + 1 + max( \sum\limits_{x\in S}b_x,\sum\limits_{x\notin S}b_x)\) 。(S 为集合)

考虑将 b 从小到大排序,找到最后一个位置 \(p\) 使得\(\sum\limits_{i=1}^{p}b_i ≤\frac{sum}{2}\)

先选上所有 \(p\) 之前的元素,此时的总和与 \(\frac{sum} {2}\) 差值不超过 \(m\)。(每个元素值均小于 \(m\)

我们此时我们进行调整,在总和比 \(\frac{sum}{2}\) 大时去掉 \(p\) 之前选的某个值,在总和比 \(\frac{sum}{2}\) 小的时候加入 \(p\) 之后的某个值,那么我们调整过程中出现的值与 \(\frac{sum}{2}\) 差值都不会超过 \(m\)

我们从 \(p\)\(n\) 进行调整,加入当前的值或者删除 \(p\) 之前的值。

考虑一个 dp,设 \(f_{i,j}\) 表示调整到 \(i\) ,要想凑出来 \(j + \frac{sum}{2}\) ,左端点至少在哪里(也就是删除的数都在 \(f_{i,j}\) 右边)。没有就是 \(0\)

根据上文有 \(−m \le j \le m\)。转移枚举当前值是否加入,在考虑 \(p\) 左侧新增的能删值是否删除。可以滚动数组。

由于每个数在每个 \(j\) 下只会被考虑一次是否加入删除,所以时间复杂度 \(O(nm)\),常数很小。可以通过此题。

code

啦啦啦
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4 + 10;
const int up = 10000;
int T, n, m, k;
int a[N], b[N];
int f[N], g[N];

inline void solve1()
{
    int ans = 0, ed = 0;
    ans = a[n];
    while (n && a[n] > m)
        --n;
    ed = (m + 1) * (n - 1);
    for (int i = 3; i <= n; ++i)
        ed -= a[i];
    ans = max(ans, ed);
    cout << ans << "\n";
}

inline void solve2()
{
    int ans = 0, mx, ed = 0;
    mx = a[n];
    while (n && a[n] > m)
        --n;
    if (n <= 2)
    {
        cout << mx << "\n";
        return;
    }
    if (n <= 4)
    {
        cout << max(m + 1, mx) << "\n";
        return;
    }
    int sum = 0, cnt = 0;
    for (int i = 5; i <= n; ++i)
        b[++cnt] = m + 1 - a[i], sum += b[cnt];
    n = cnt;
    sort(b + 1, b + 1 + n);
    int id = 1, now = 0;
    while (id <= n && now + b[id] <= sum / 2)
        now += b[id], ++id;
    memset(f, 0, sizeof(f));
    memset(g, 0, sizeof(g));
    g[now - sum / 2 + up] = id;

    for (int i = id; i <= n; ++i)
    {
        memcpy(f, g, sizeof(f));
        for (int j = 0; j < up; ++j)
            f[j + b[i]] = max(f[j + b[i]], g[j]);

        for (int j = (up << 1); j > up; --j)
            for (int k = g[j]; k < f[j]; ++k)
                f[j - b[k]] = max(f[j - b[k]], k);

        memcpy(g, f, sizeof(g));
    }

    for (int i = up; ~i; --i)
        if (g[i])
        {
            ans = m + 1 + sum - (sum / 2 + i - up);
            break;
        }

    cout << max(mx, ans) << "\n";
}

signed main()
{
    freopen("money.in", "r", stdin);
    freopen("money.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> T;
    while (T--)
    {
        cin >> n >> m >> k;
        for (int i = 1; i <= n; ++i)
            cin >> a[i];
        sort(a + 1, a + 1 + n);
        if (k == n - 1)
            solve1();
        else
            solve2();
    }
    return 0;
}

t3

神秘。

咕。

t4

咕。

posted @ 2025-10-20 21:54  HS_fu3  阅读(22)  评论(2)    收藏  举报