2026/09/28 模拟赛总结

NOIP 模拟赛第十二场,可惜这场前三题没有 Div.2 难度高。

预估分数:\(100+[90,100]+100+32\)。

实际分数:\(100+100+100+32,rk3/16\)。

Lemon 评测机:\(100+100+100+32,rk3/16\)。

其实这个分数是大众分。

T4 本来可以多拿点分到 \(68pts\) 的,没仔细想,其实是前三题做的还是太慢了。

T1

题目大意:

你有一个 \(n\) 个结点的树,你需要为这棵树的每条边赋上一个权值 \(0/1\),使得对于任意两个整数 \(i,j\) 满足 \(1 \le i \le j \le n\),路径 \((i,j)\) 的贡献和最大,定义一条路径的贡献为边权的异或和。注意你需要输出边权赋权方案和方案的贡献和。
\(n \le 10^6\)

由于这题没有大样例,赛时就在想是否有规律。

然后暴力打表打出的表就是:无论树是什么形态,答案都是 \(\left\lfloor\frac{n}{2}\right\rfloor \times (n - \left\lfloor\frac{n}{2}\right\rfloor)\)。

考虑如何构造。首先显然边权全为 \(1\) 是不行的,菊花图就能卡。因此我的思路是每一条路径最多经过 \(2\) 个权值为 \(1\) 的边。我们令权值为 \(1\) 的边为断边,于是一棵树就会被断成若干棵子树。并且通过适当构造使得这些子树要么包含根结点,要么有且只和根结点相邻。此时不难发现,如果一条路径要得到贡献,必须得是包含根结点的连通块上的一点走到其它子树上的一点。因此,我们只要将包含根结点的连通块大小设成 \(n - \left\lfloor\frac{n}{2}\right\rfloor\),就能得到符合路径个数,惊讶的发现这恰好就是 \(\left\lfloor\frac{n}{2}\right\rfloor \times (n - \left\lfloor\frac{n}{2}\right\rfloor)\)。于是我们就凭借规律和直觉写完了这题。

#include <iostream>
#include <vector>
#include <algorithm>
#define int long long
using namespace std;
int n,siz[1000005],eg[1000005],ans;
int id = 1,din[1000005];
struct node{
    int v,id,nxt;
}ve[3000005];
void add(int x,int y,int z)
{
    ve[++id] = {y,z,din[x]};
    din[x] = id;
}
inline void dfs(int x,int fa)
{
    siz[x] = 1;
    for(int i = din[x];i;i = ve[i].nxt)
    {
        int to = ve[i].v;
        if(to == fa)continue ;
        dfs(to,x);
        siz[x] += siz[to];
    }
}
inline void dfs2(int x,int fa,int duan)
{
    for(int i = din[x];i;i = ve[i].nxt)
    {
        int to = ve[i].v;
        if(to == fa)continue ;
        if(siz[to] <= duan)duan -= siz[to],eg[ve[i].id] = 1;
        else
        {
            dfs2(to,x,duan);
            return ;
        }
        if(!duan)return ;
    }
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    freopen("easy.in","r",stdin);
    freopen("easy.out","w",stdout);
    cin >> n;
    for(int i = 1,u,v;i < n;i ++)
    {
        cin >> u >> v;
        add(u,v,i),add(v,u,i);
    }
    dfs(1,0);
    dfs2(1,0,n / 2);
    if(n % 2)cout << (n / 2) * (n / 2 + 1) << '\n';
    else cout << n * n / 4 << '\n';
    for(int i = 1;i < n;i ++)cout << eg[i] << " ";
}

然而后面查看 TJ 后发现 T1 有更好的解法。

就是说,你直接令 \(\left\lfloor\frac{n}{2}\right\rfloor\) 个点的点权为 \(1\),其余点权为 \(0\),如果一条边连接的两个点恰好是一个点权为 \(1\) 一个为 \(0\),则该边的边权为 \(1\),否则为 \(0\)。

简单说一下正确性就是根 \(rt\) 到结点 \(u\) 的路径异或和就恰好等于 \(rt\) 的点权异或 \(u\) 的点权,因此一条路径的异或和就恰好等于 \(u\) 的点权异或 \(v\) 的点权,所以我们只需要将点权一半设为 \(0\),一半设为 \(1\),一定是最优的。

T2

题目大意:
你有三堆硬币,其中第 \(1\) 堆硬币从上至下的编号依次为 \(1,2,...,n\),第二、三堆为空。你可以进行的操作是选择第 \(x\) 堆最上层的硬币移动到第 \(x \bmod 3 + 1\) 堆硬币上,条件是第 \(x \bmod 3 + 1\) 堆硬币最上方的硬币编号大于你移动的硬币编号。请分别求出所有硬币全部移动到第二堆的最小操作步数和全部移动到第三堆的最小操作步数,\(T\) 组数据。
\(1 \le T \le 1.2 \times 10^6,1 \le n \le 10^{12}\)

先说 \(\mathcal{O}(n)\) 做法。

设当 \(n = i - 1\) 时第一个答案为 \(a_1\),第二个答案为 \(b_1\);当 \(n = i\) 时第一个答案为 \(a_2\),第二个答案为 \(b_2\)。

首先推一下 \(a_2\):首先前面步骤和 \(b_1\) 操作完全一样,然后将编号为 \(i\) 的硬币移动到第二堆,再将 \(i - 1\) 个硬币从第三堆移动到第二堆,因此递推式为 \(a_2 = 2 \times b_1 + 1\)。

然后推一下 \(b_2\):前面步骤和 \(b_1\) 操作完全一样,然后将编号为 \(i\) 的硬币移动到第二堆,再将 \(i - 1\) 个硬币从第三堆移动到第二堆,然后将编号为 \(i\) 的硬币移动到第三堆,最后把前面 \(i - 1\) 个硬币从第二堆移动到第三堆,因此递推式为 \(a_3 = 2 \times b_1 + a_1 + 2\)。

初始值 \(a_0 = b_0 = 0\),然后你随便拿这两个简单递推式即可做到 \(\mathcal{O}(n)\)。

但显然这个递推式太板了,矩阵优化一下就行了,答案就是 \(\begin{bmatrix}1&2&0\end{bmatrix} \times \begin{bmatrix}0&1&0\\2&2&0\\1&2&1\end{bmatrix}^{n - 1}\) 的第一项和第二项。因此你会得到 \(90\) 分,因为复杂度是 \(\mathcal{O}(T \times 3^3 \log n)\) 的。

然后这里正解是用一个叫做"光速矩阵幂"的东西,就是你把 \(A,A^{2},.\!.\!.,A^{P-1},A^{P},A^{2P},A^{3P},.\!.\!.,A^{P^2}\) 全部预处理出来,其中 \(A\) 为转移矩阵,于是查询时直接选两个矩阵乘起来就能 \(\mathcal{O}(T \times 3^3)\) 得出答案。

但因为 \(T \le 10^6\) 且 \(n \le 10^{12}\),所以正解和我的经过优化的错解竟然跑的差不多快(我预处理的是 \(A\),\(A^2\),\(A^4\),...),数据强时同时被卡常,数据水时同时通过,但最后发现数据中 \(T\) 开到 \(2 \times 10^6\) 了,因此最终我在 Lemon 评测机和 oj 中都是满分。

#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int mod = 998244353;
int T,n,ans1,ans2;
struct mat
{
    int n,m;
    int a[4][4];
    void init(int x,int y)
    {
        n = x,m = y;
        for(int i = 1;i <= n;i ++)
            for(int j = 1;j <= m;j ++)a[i][j] = 0;
    }
    inline mat operator * (const mat &w)const
    {
        mat ans;
        ans.init(n,w.m);
        for(int i = 1;i <= n;i ++)
            for(int j = 1;j <= w.m;j ++)
            {
                for(int x = 1;x <= m;x ++)
                    ans.a[i][j] = (ans.a[i][j] + a[i][x] * w.a[x][j]);
                ans.a[i][j] %= mod;
            }
        return ans;
    }
}A,B,D,E[80];
mat fpow(mat x,int y)
{
    mat z;
    z.init(x.n,x.n);
    for(int i = 1;i <= x.n;i ++)z.a[i][i] = 1;
    while(y)
    {
        if(y & 1)z = z * x;
        x = x * x;
        y >>= 1;
    }
    return z;
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    freopen("riemannian.in","r",stdin);
    freopen("riemannian.out","w",stdout);
    cin >> T;
    A.init(1,3),B.init(3,3);
    A.a[1][1] = 1,A.a[1][2] = 2,A.a[1][3] = 1;
    B.a[1][2] = B.a[3][1] = B.a[3][3] = 1,B.a[2][1] = B.a[2][2] = B.a[3][2] = 2;
    D = A,E[0] = B;
    for(int i = 1;i <= 50;i ++)E[i] = E[i - 1] * E[i - 1];
    while(T --)
    {
        A = D;
        cin >> n;n --;
        while(n)
        {
            int ad = __builtin_ctzll(n);
            A = A * E[ad];
            n -= (1LL << ad);
        }
        ans1 ^= A.a[1][1],ans2 ^= A.a[1][2];
    }
    cout << ans1 << " " << ans2;
}

T3

题目大意:

有一个长度为 \(n\) 的序列 \(a_0,a_1,...,a_{n - 1}\),\(q\) 次询问每次给定一个整数 \(k\),你需要将 \(a\) 进行重排列使得 \(a_0 \times a_k + a_1 \times a_{k + 1} +\,.\!.\!. + a_{n - k - 1} \times a_{n - 1} + a_{n - k} \times a_0 +\,.\!.\!. + a_{n - 1} \times a_{k - 1}\) 的值最大,输出这个最大值。

\(n,q \le 5000,0 \le k < n,1 \le a_i \le 10^5\)

题解复杂度为什么会出现 \(\mathcal{O}(n^{\frac{7}{3}})\),拉完了。实际上 \(\mathcal{O}(nq)\) 做法非常容易想到。

首先我们发现有些数是互不关联的,具体而言它们会构成 \(\gcd(n,k)\) 个环,第 \(i\) 个环的起点是 \(a_{i - 1}\),然后依次是 \(a_{i + k - 1},a_{i + 2 \times k - 1}\),等等。

题解竟然还对这写环一起考虑,实际上是完全可以贪心做的。我们先做只有一个环的情况。我们对 \(a\) 数组从大到小排序。首先 \(a_0\) 可以放在任意位置,而因为我们想让相邻乘积尽可能大所以 \(a_1\) 得放在 \(a_0\) 相邻位置,\(a_2\) 也放在 \(a_0\) 的相邻位置。所以 \(a_3\) 与 \(a_1\) 相邻,...,以此类推。

然后来到 \(m\) 个环的情况,这时候我们使用贪心:将权值最大的 \(\frac{n}{\gcd(n,k)}\) 个数构成一个环,剩下的数中权值最大的 \(\frac{n}{\gcd(n,k)}\) 个数构成一个环,...,以此类推。

每个环贪心求一下最大值就做完了,复杂度 \(\mathcal{O}(nq)\)。

#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
int n,a[200005],m;
signed main()
{
    freopen("algebraic.in","r",stdin);
    freopen("algebraic.out","w",stdout);
    cin >> n;
    for(int i = 1;i <= n;i ++)cin >> a[i];
    sort(a + 1,a + n + 1,greater<int>());
    cin >> m;
    for(int i = 1,k;i <= m;i ++)
    {
        cin >> k;
        int len = n / __gcd(n,k),ans = 0;
        for(int j = 1;j <= n;j += len)
        {
            if(len == 1)
            {
                ans += a[j] * a[j];
                continue ;
            }
            int zuo = a[j],you = a[j];
            for(int l = j + 1;l < j + len;l ++)
            {
                if(zuo > you)ans += zuo * a[l],zuo = a[l];
                else ans += you * a[l],you = a[l];
            }
            ans += zuo * you;
        }
        cout << ans << '\n';
    }
}

T4

题目大意:

你有一个 \(n \times n\) 的棋盘,求对棋盘黑白染色的方案数,使得该棋盘任意一个 \(2 \times 2\) 的子矩阵的黑色格子数量均为偶数,且没有一个面积大于 \(k\) 的子矩阵颜色全部相同,对 \(10^4 + 7\) 取模。
\(n \le 5 \times 10^7,k \le 10^5\),时限 6s

这题 100pts 做法还是太困难了,但是我觉得 \(\mathcal{O}(n\sqrt{k})\) 的 \(80pts\) 做法也是很巧妙的,至少比我 \(32pts\) 的暴力好。

实际上此题还是需要一些观察力。

暴力我不说了。

这题有一个关键性质:如果棋盘的第一行以及第一列的颜色确定了,那么整个棋盘的颜色也就确定了,可以由前面递推得到。

于是复杂度变成了 \(\mathcal{O}(2^{n + m})\),虽然比 \(\!32\)分高就是了。

更关键的性质就是:棋盘的最大颜色相同矩阵面积等于行的极长连续段乘上列的极长连续段。看这个图能够秒懂。

image

然后我们就有个非常简单的思路:设 \(dp_i\) 表示一行 \(n\) 个数最长连续段个数等于 \(i\) 的个数的方案数,用动态规划解决。

但是我们发现恰好实在是太难写了,于是就将状态改为一行 \(n\) 个数最长连续段个数不超过 \(i\) 的个数的方案数。然后原问题可以直接用 \(dp_i - dp_{i - 1}\) 求得。

然后就是经典的 dp 环节,令设一个 \(dp2_j\) 表示考虑到第 \(j\) 位最长连续段个数不超过 \(i\) 的个数的方案数,则 \(dp2_j\) 就等于 \(\sum_{k = j - i}^{j-1}dp2_k\)。因此 \(dp_i = dp2_n\)。

计 \(q_i = \sum_{1}^i dp_i\),于是计算 \(\sum dp_i \times q_{\left\lfloor\frac{k}{i}\right\rfloor}\) 即可,注意答案除以 \(2\),因为行和列共用了一个格子 \((1,1)\)。

代码(68pts)

#include <iostream>
#include <vector>
using namespace std;
int n,kk,dp2[20000005],dp3[20000005];
vector<int>dp;
const int mod = 1e4 + 7;
int qpow(int x,int y)
{
    int z = 1;
    while(y)
    {
        if(y & 1)z = z * x % mod;
        x = x * x % mod;
        y >>= 1;
    }
    return z;
}
int main()
{
    freopen("chess.in","r",stdin);
    freopen("chess.out","w",stdout);
    cin >> n >> kk;
    if(kk == 1)
    {
        cout << 2;
        return 0;
    }
    if(kk >= n * n)
    {
        int ans = 2;
        for(int i = 2;i <= n;i ++)ans = ans * 4 % mod;
        cout << ans;
        return 0;
    }
    int ans = 0;
    dp.resize(kk + 2);
    for(int i = 1;i <= kk;i ++)
    {
        dp2[0] = 2;
        for(int j = 1;j <= n;j ++)
        {
            dp3[j] = (dp2[j - 1] - (j - i - 1 < 0 ? 0 : dp2[j - i - 1])) % mod;
            dp2[j] = dp2[j - 1] + dp3[j];
        }
        dp[i] = dp3[n];
    }
    for(int i = kk;i >= 1;i --)dp[i] = (dp[i] - dp[i - 1] + mod) % mod;
    dp2[0] = 0;
    for(int i = 1;i <= kk;i ++)dp2[i] = (dp2[i - 1] + dp[i]) % mod;
    for(int i = 1;i <= kk;i ++)
    {
        int mx = kk / i;
        ans += dp2[mx] * dp[i];
        ans %= mod;
    }
    cout << ans * qpow(2,mod - 2) % mod;
}

优化

注意到我们需要的 \(dp_i\) 只需要 \(\sqrt{k}\) 个,因此直接在上一版的代码上套一个整除分快,拿到 \(80\text{pts}\)。

这里复杂度是 \(\mathcal{O}(n\sqrt{k})\) 的。

posted @ 2026-09-29 22:10  wuyixiang  阅读(13)  评论(0)    收藏  举报