2025-08-18 NOIP && NOI 模拟赛题解

T1

Rainbow的信号

拆贡献,然后拆位。异或记个后缀和边扫边算;与和或考虑极长的连续 0/1 段。

注意可能爆 long long 范围以及精度问题。

T2

Freda的传呼机

正解直接建圆方树,然后把路径沿 lca 拆成两条竖着的路径,这两条路径进入的环最后一定从方点的父亲出来,于是给每个除方点父亲的圆点的父边赋值为到方点父亲的最短路,lca 处如果是方点就拆开。

但是这个做法还是太没意思了,注意到范围很小,所以出题人肯定不想让我们用正经的做法。我们考虑直接对询问的某一个点跑 spfa,然后考虑怎么优化常数。

首先队列手写。然后把询问丢进桶里,然后尽可能抽出覆盖询问多的点跑,这样就能跑尽可能少次 spfa。

接下来是正经的内容。

那么最多会跑多少次呢?首先转化成一个无向图, \(n\)\(n\) 边,求最小点覆盖。结论是当 \(n\) 为 3 的倍数时,最多跑 \(\frac{2n}{3}\) 次。

首先有个结论,就是一个连通图的最小点覆盖不会超过 \(\lceil \frac{边数}{2} \rceil\)。证明考虑跑出 dfs 树,然后依次删掉两条共顶点的边,然后随便归纳一下就好了。

回到原问题上,那也就是说,既然我们想让点覆盖尽可能大,那尽量让每个连通块都有奇数条边,这样点覆盖就有可能超过边数除以 2。

也就是说要找到一个东西满足以下条件:

  • 连通
  • 最小点覆盖严格大于边数一半

而性价比最高的就是三元环了。因为用三个边换了两个点覆盖。而比如五元环就是三个点覆盖,没有它值。

所以证明了当 \(n\) 为 3 的倍数时,最小点覆盖最大为 \(\frac{2n}{3}\)。构造一堆独立的三元环即可。

这样最坏我把我自己卡到了 6664 次 spfa,由于仙人掌本身就松弛次数少,所以没什么优化空间,这个乱搞应该也是被卡掉了。(好的评测机应该也能撑)

然而随机数据下期望跑 4000 次左右,还是能通过的。(不过比较奇怪的是树跑的比仙人掌要慢近 200ms,按理来讲不会再次松弛 spfa,是要比仙人掌快的,,,当然询问数据强弱差异也不是没有可能)

T3

Circle Game

转化一下问题,你有 n 个数,范围 \([0,2^m)\),然后你可以给所有数加 \([0,T]\) 次,问最后异或和为 \(S\) 的情况数。

\(n\le 10^5, m\le 50, T\le 10^{18}\)

非常套路的 dp 题啊,由于涉及进位所以从低位 dp,设 \(f_{k,x}\) 表示考虑完了前 \(k\) 位(与 \(S\) 相同),有 \(x\) 个数向 \(k+1\) 位进位。

考虑转移。现在有个问题是怎么知道哪 \(x\) 个数进位了。但是我们仔细一想啊,产生进位等价于 \(a_i\ge 2^{k}\),既然加的是同一个值,那产生进位的一定是值域上(\([0,2^{m})\))的一个后缀啊!于是我们将 \(a_i\) 按照模 \(2^m\) 的值降序排序,取前几大的即可(同一个值得取完)。然后就做完了。另外代码中用了基数排序实现,边做边排很方便。

// Author: Aquizahv
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 1e5 + 5, M = 55;
int n, m, f[M][N], s[M][2], fix[M];
ll a[N], S, T;

bool eq(ll x, ll y)
{
    return (x & 1) == (y & 1);
}

ll solve()
{
    memset(f, 0, sizeof(f));
    vector<ll> b[2];
    if (fix[0] != 1 && eq(s[0][1], S))
        f[0][0] = 1;
    if (fix[0] != 0 && eq(s[0][0], S))
        f[0][s[0][1]] = 1;
    for (int k = 1; k < m; k++)
    {
        b[0].clear(), b[1].clear();
        for (int i = 1; i <= n; i++)
            b[(a[i] >> (k - 1)) & 1].push_back(a[i]);
        int pos = 0;
        for (auto x : b[0])
            a[++pos] = x;
        for (auto x : b[1])
            a[++pos] = x;
        int c[2] = {0, 0};
        int all = (1ll << k) - 1;
        for (int i = n + 1; i >= 1; i--)
        {
            if (i <= n)
                c[(a[i] >> k) & 1]++;
            if (i == n + 1 || i == 1 || (a[i] & all) != (a[i - 1] & all))
            {
                // 0
                if (fix[k] != 1 && eq(c[0] + s[k][1] - c[1], S >> k))
                    f[k][c[1]] += f[k - 1][n - i + 1];
                // 1
                if (fix[k] != 0 && eq(c[1] + s[k][0] - c[0], S >> k))
                    f[k][s[k][1] + c[0]] += f[k - 1][n - i + 1];
            }
        }
    }
    ll res = 0;
    for (int i = 0; i <= n; i++)
        res += f[m - 1][i];
    return res;
}

int main()
{
    cin >> n >> m >> S >> T;
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", a + i);
        for (int k = 0; k < m; k++)
            s[k][(a[i] >> k) & 1]++;
    }
    for (int k = 0; k < m; k++)
        fix[k] = -1;
    ll ans = T / (1ll << m) * solve();
    T %= 1ll << m;
    for (int k = 0; k < m; k++)
        fix[k] = (T >> k) & 1;
    ans += solve();
    for (int k = 0; k < m; fix[k++] = -1)
        if (T & (1ll << k))
        {
            fix[k] = 0;
            ans += solve();
        }
    memset(fix, 0, sizeof(fix));
    cout << ans - solve() << endl;
    return 0;
}

T4

[Tree]

给定一棵有 \(n\) 个节点的带权无根树。现在,你需要选择一些互不相交(包括端点)的路径。如果你选择了 \(k\) 条路径,且覆盖的点权和为 \(S\),你的得分为 \(\frac{S}{k+1}\)

另外,在选取路径之前,你必须执行一次如下操作(操作分为三个步骤):

  1. 选定一个参数 \(C\),满足 \(C\in [0,T]\)
  2. 将所有点权 \(+C\)
  3. 将所有点权对 \(lim\) 取模

其中,\(T\)\(lim\) 的值已经被给出。你的任务就是求出可能的最高得分。

看到分数,考虑二分答案 mid,那么合法性就是 \(\frac{S}{k+1} \ge mid\),即 \(S-kmid\ge mid\)。于是就不用记覆盖链数的状态了,覆盖一个减掉一个 mid 即可。

考虑 dp,设 \(f_{u,0/1/2}\) 表示考虑到 \(u\) 这一位,其中 \(u\) 不选/选了且父边不选/选了且父边选 的方案数。

考虑在一个终止的地方减掉 mid,也就是说如果 \(u\) 选了且父边不选则减掉。不难发现这样是对的。

\[\begin{cases} f_{u,0}&=\sum \max(f_{v,0}, f_{v,1})\\ f_{u,1}&=a_u + \sum \max_{f_{v,2}取的次数\le 2}(f_{v,0}, f_{v,1}, f_{v,2})-mid\\ f_{u,2}&=a_u + \sum \max_{f_{v,2}取的次数\le 1}(f_{v,0}, f_{v,1}, f_{v,2})\\ \end{cases} \]

其中 \(f_{u,1}\) 最多可以有两个儿子连上,\(f_{u,2}\) 最多可以有一个儿子。

维护 \(f_{v,2}-\max(f_{v,0}, f_{v,1})\) 的最大次大值即可转移。

但是如果我们不能枚举每一个 \(C\in [0,T]\),注意到如果当前所有数都能 \(+1\),那么这一组数是没用的。所以有用的 \(C\) 只有 \(\min(lim-1-a_i,T)\) 这些,个数 \(O(n)\)

于是时间复杂度 \(O(n^2\log V)\)

但是这个题卡的很恶心啊,所以进行一些优化:二分左端点设为 ans;如果当前 \(check(ans)\) 的值为假则直接 continue;;将可能的 \(C\) 去重、shuffle。

// Author: Aquizahv
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 5005;
const double eps = 1e-9;
const double Eps = 1e-6;
int n;
ll MOD, T, a[N], b[N], c[N];
double f[N][3];
vector<int> g[N];

void dfs(int u, int fa, double mid)
{
    f[u][0] = 0;
    f[u][1] = b[u] - mid;
    f[u][2] = b[u];
    double mx[2] = {-1, -1};
    for (auto v : g[u])
        if (v != fa)
        {
            dfs(v, u, mid);
            double x = max(f[v][0], f[v][1]);
            double y = f[v][2] - x;
            if (y > mx[0])
                mx[1] = mx[0], mx[0] = y;
            else if (y > mx[1])
                mx[1] = y;
            f[u][0] += x;
            f[u][1] += x;
            f[u][2] += x;
        }
    if (mx[0] > 0)
    {
        f[u][1] += mx[0], f[u][2] += mx[0];
        if (mx[1] > 0)
            f[u][1] += mx[1];
    }
}

bool check(double mid)
{
    dfs(1, 0, mid);
    return max(f[1][0], f[1][1]) > mid - eps;
}

int main()
{
    cin >> n >> MOD;
    for (int i = 1; i <= n; i++)
        scanf("%lld", a + i);
    int u, v;
    for (int i = 1; i < n; i++)
        scanf("%d%d", &u, &v), g[u].push_back(v), g[v].push_back(u);
    cin >> T;
    double ans = 0;
    for (int i = 1; i <= n; i++)
    {
        c[i] = MOD - 1 - a[i];
        c[i] = min(c[i], T);
    }
    sort(c + 1, c + n + 1);
    int nn = unique(c + 1, c + n + 1) - c - 1;
    random_shuffle(c + 1, c + nn + 1);
    for (int i = 1; i <= nn; i++)
    {
        for (int j = 1; j <= n; j++)
            b[j] = (a[j] + c[i]) % MOD;
        if (!check(ans))
            continue;
        double l = ans, r = n * MOD;
        while (r - l > Eps)
        {
            double mid = (l + r) / 2;
            if (check(mid))
                l = mid;
            else
                r = mid;
        }
        ans = max(ans, l);
    }
    printf("%.9lf\n", ans);
    return 0;
}
posted @ 2025-08-19 13:48  Aquizahv  阅读(39)  评论(1)    收藏  举报