2021年12月4日小视野模拟赛解题报告

12   月   4   日 小 视 野 模 拟 赛 \rm 12 ~ 月 ~ 4 ~ 日小视野模拟赛 12  4 

A. Clock Sequence

科学家温斯顿定义了一个无限重复的数列:1234321234321234321……,并将其称为时钟序列。

他发现可以将数列人为分成几段:

1, 2, 3, 4, 32, 123, 43, 2123, 432, 1234, 32123, ...

他又定义了新数列中第 n n n 项为 V n V_n Vn,这样分组能够满足 V n V_n Vn 的数字和等于 n n n

例如, V 2 = 2 , V 7 = 43 , V 11 = 32123 V_2=2,V_7=43,V_{11}=32123 V2=2,V7=43,V11=32123

请帮助他求出数列 V V V 的前 n n n 项和。

sol

找规律即可。

#include <bits/stdc++.h>

#define int long long

const int MOD = 123454321, k = 96007682;

using namespace std;

int n, ans, a, b;

int sum1[16] = {0, 1, 3, 6, 10, 42, 165, 208, 2331, 2763, 3997, 36120, 79332, 113653, 137085, 260517};

int sum2[16] = {0, 234321, 577533, 1009656, 1330890, 1454322, 1886445, 2098788, 2530911, 2654343, 2975577, 3407700, 3750912, 3985233, 4108665, 4232097};

int qpow(int x, int y)
{
    int ans = 1ll;
    while (y)
    {
        if (y & 1)
            ans = ans * x % MOD;
        x = x * x % MOD;
        y >>= 1;
    }
    return ans;
}

signed main()
{
    scanf("%lld", &n);
    a = n / 15, b = n % 15;
    int a1 = (qpow(1000000, a) - 1) * k % MOD;
    int a2 = (a1 - a + MOD) % MOD * k % MOD;
    int a3 = (qpow(1000000, a + 1) - 1) * k % MOD;
    a3 = (a3 - a1 + MOD) % MOD;
    ans = (a1 * sum1[15] % MOD + a2 * sum2[15] % MOD + a3 * sum1[b] % MOD + a1 * sum2[b] % MOD) % MOD;
    printf("%lld\n", ans);
    return 0;
}

B. 灯

突然,牛棚的电源跳闸了,所有的灯都被关闭了。你需要将所有的灯都给重新开起来!

牛棚中一共有 N ( 1 < = N < = 35 ) N(1 <= N <= 35) N1<=N<=35 盏灯,编号为 1 1 1 N N N。这些灯被置于一个非常复杂的网络之中。有 M M M条很神奇的无向边,每条边连接两盏灯。

每盏灯上面都带有一个开关。当按下某一盏灯的开关的时候,这盏灯本身,还有所有有边连向这盏灯的灯的状态都会被改变。状态改变指的是:当一盏灯是开着的时候,这盏灯被关掉;当一盏灯是关着的时候,这盏灯被打开。

问最少要按下多少个开关,才能把所有的灯都给重新打开。

sol

由于 N N N 非常小,可以考虑状压加暴力搜索。

但复杂度是 O ( 2 25 ) \mathcal O(2^{25}) O(225) 的必然超时。

考虑优化,折半搜索,分成 2 17 2^{17} 217 2 18 2^{18} 218 两个部分搜索。

先搜前 17 17 17 盏灯,将状态保存起来。

再搜后 18 18 18 盏灯,看当前状态是否可以和之前状态的异或值是否为 2 n − 1 2^n-1 2n1 a n s ans ans 取最小即可。

#include <bits/stdc++.h>

#define int long long

using namespace std;

int n, m;

int ans = INT_MAX;

int mid, cnt;

int vis[1007], pos[1007];

vector<int> d[1007];

struct abc
{
    int num, pos;
};

struct cmp
{
    bool operator()(const abc &a, const abc &b) const
    {
        return a.pos < b.pos;
    }
};

vector<abc> vec;

map<int, int> mp;

void dfs(int x, int num, int p)
{
    if (!mp[p])
        vec.push_back({num, p}), mp[p] = vec.size();
    else
        vec[mp[num]].num = min(vec[mp[num]].num, num);
    if (x == mid + 1ll)
        return;
    dfs(x + 1ll, num, p);
    dfs(x + 1ll, num + 1ll, p ^ pos[x]);
}

void dfss(int x, int num, int p)
{
    abc tmp = {num, p ^ ((1ll << n) - 1ll)};
    auto it = lower_bound(vec.begin(), vec.end(), tmp, cmp());
    if (it != vec.end())
        if (((*it).pos ^ p) == ((1ll << n) - 1ll))
            ans = min(ans, (*it).num + num);
    if (x == n + 1ll)
        return;
    dfss(x + 1ll, num, p);
    dfss(x + 1ll, num + 1ll, p ^ pos[x]);
}

signed main()
{
    scanf("%lld%lld", &n, &m);
    mid = (n + 1ll) >> 1ll;
    for (int i = 1ll; i <= m; i++)
    {
        int u, v;
        scanf("%lld%lld", &u, &v);
        d[u].push_back(v);
        d[v].push_back(u);
    }
    for (int i = 1ll; i <= n; i++)
    {
        for (int j : d[i])
            vis[j] = 1ll;
        vis[i] = 1ll;
        for (int j = 1ll, k = 1ll; j < (1ll << n); j <<= 1ll, ++k)
        {
            if (vis[k])
                pos[i] += j, vis[k] = 0;
        }
    }
    dfs(1ll, 0, 0);
    sort(vec.begin(), vec.end(), cmp());
    dfss(mid + 1ll, 0, 0);
    printf("%lld\n", ans);
    return 0;
}

C. 硬币游戏

初始时,一个有 N N N 枚硬币的堆栈放在地上,从堆顶数起的第 i i i 枚硬币的币值为 C i C_i Ci

开始玩游戏时,第一个玩家可以从堆顶拿走一枚或两枚硬币。

在每一轮中,当前的玩家至少拿走一枚硬币,至多拿走对手上一次所拿硬币数量的两倍。

当没有硬币可拿时,游戏结束。

两个玩家都希望拿到最多钱数的硬币。请问,当游戏结束时,第一个玩家最多能拿多少钱呢?

sol

这道题目我们可以选择倒着推:因为最后一个因此必然是要取完的,因此可以确定状态一定是可行的。

我们给每一个金币自底向上标号为 1 ∼ i 1 \sim i 1i

状态:设 f i , j f_{i,j} fi,j 表示某一方取完以后还剩下编号为 1 ∼ i 1 \sim i 1i 的金币,上一个人取了 j j j 枚金币的最大取值。

转移:对于每一个状态我们必然需要知道一个当前所取的金币数 k k k,必然和 f i − k , k f_{i-k,k} fik,k 有关,表示 1 ∼ i 1 \sim i 1i 堆中取了 k k k,还剩下 1 ∼ i − k 1 \sim i-k 1ik 枚金币;那么这个人当前取的金币数就是总金币减去下一个决策的金币。

可得状态转移方程:
f i , j = max ⁡ ( f i , j , s u m i − f i − k , k ) f_{i,j} = \max(f_{i,j}, sum_i - f_{i - k,k}) fi,j=max(fi,j,sumifik,k)
优化:观察每一个 f i , j f_{i,j} fi,j,对于每一个 f i , j − 1 f_{i,j-1} fi,j1,多余的决策只有选 2 j − 1 2j-1 2j1 枚金币和 2 j 2j 2j 枚,只要 k k k 等于这两个值做两遍转移,和原来求最大值取一个最大值即可。

#include <bits/stdc++.h>

using namespace std;

const int _ = 2005;

int n, a[_], s[_], f[_][_];

signed main()
{
    scanf("%d", &n);
    for (int i = n; i >= 1; --i)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n; ++i)
        s[i] = s[i - 1] + a[i];
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            f[i][j] = f[i][j - 1];
            int k = 2 * j - 1;
            if (k <= i)
                f[i][j] = max(f[i][j], s[i] - f[i - k][k]);
            k++;
            if (k <= i)
                f[i][j] = max(f[i][j], s[i] - f[i - k][k]);
        }
    }
    printf("%d\n", f[n][1]);
    return 0;
}

D. Gcd

给定整数 N N N,求 1 ≤ x , y ≤ N 1 \leq x,y \leq N 1x,yN gcd ⁡ ( x , y ) \gcd(x,y) gcd(x,y) 为素数的数对 ( x , y ) (x,y) (x,y) 有多少对。

sol
算法一

我会欧拉函数!!!

gcd ⁡ ( x , y ) = k \gcd(x,y)=k gcd(x,y)=k,那么 g c d ( x k , y k ) = 1 gcd(\frac{x}{k},\frac{y}{k})=1 gcd(kx,ky)=1

根据 φ \varphi φ 的定义, q ≥ p q \ge p qp,满足 g c d ( q , p ) = 1 gcd(q,p)=1 gcd(q,p)=1 p p p 的个数即为 φ ( q ) \varphi(q) φ(q)

那么满足 gcd ⁡ ( x , y ) = k \gcd(x,y)=k gcd(x,y)=k 的二元组个数为 2 ∗ ∑ i = 1 N / k φ ( i ) − 1 2* \sum\limits_{i=1}^{N/k}\varphi(i)-1 2i=1N/kφ(i)1(其中 gcd ⁡ ( k , k ) = k \gcd(k,k)=k gcd(k,k)=k 这组多算了一次。

用前缀和维护即可。

枚举所有个数计算贡献即可。

#include <bits/stdc++.h>

const int _ = 1e7 + 7;

using namespace std;

int prime[_], phi[_];

long long g[_ / 2];

long long ans;

int n, tot;

signed main()
{
    scanf("%d", &n);
    phi[1] = 1;
    g[1] = 1;
    for (int i = 2; i <= n; i++)
    {
        if (!phi[i])
        {
            prime[++tot] = i;
            phi[i] = i - 1;
        }
        for (int j = 1; j <= tot && prime[j] * i <= n; j++)
        {
            if (i % prime[j] == 0)
            {
                phi[i * prime[j]] = phi[i] * prime[j];
                break;
            }
            phi[i * prime[j]] = phi[i] * (prime[j] - 1);
        }
        g[i] = g[i - 1] + phi[i];
    }
    for (int i = 1; i <= tot; i++)
        ans += g[n / prime[i]];
    printf("%lld\n", ans * 2 - tot);
    return 0;
}
算法二

我会莫比乌斯反演!!!

我们令 f ( n ) f(n) f(n) 表示两个数最大公因数为 n n n 的方案数, F ( n ) F(n) F(n) 表示两个数的 gcd ⁡ \gcd gcd n n n 的倍数的方案数。

显然 f ( x ) = ∑ x ∣ d μ ( d x ) F ( d ) f(x)=\sum\limits_{x|d}\mu(\frac{d}{x})F(d) f(x)=xdμ(xd)F(d)

F ( x ) F(x) F(x) 怎么求呢?可以先记录每个数 i i i 出现的次数 c n t i = ⌊ n i ⌋ cnt_i=\left\lfloor \frac{n}{i} \right\rfloor cnti=in,则是 x x x 的倍数的数有 ∑ x ∣ d c n t d \sum\limits_{x|d}cnt_d xdcntd 个,记为 p p p,那么 F ( x ) = ( p 2 ) F(x)=\begin{pmatrix}p\\2\end{pmatrix} F(x)=(p2),也就是从 p p p 个数中选 2 2 2 个数的方案数。

显然, A n s = ∑ i = 1 n u m [ p r i i ≤ n ] 2 ∗ ∑ p r i i ∣ d μ ( d p r i i ) F ( d ) + 1 Ans=\sum\limits_{i=1}^{num}[pri_i \leq n]{2*\sum\limits_{pri_i|d} \mu(\frac{d}{pri_i})F(d)}+1 Ans=i=1num[priin]2priidμ(priid)F(d)+1(乘 2 2 2 是因为要求无序对,加 1 1 1 是指加上 gcd ⁡ ( p r i i , p r i i ) = p r i i \gcd(pri_i,pri_i)=pri_i gcd(prii,prii)=prii 这组。

具体实现见代码。

#include <bits/stdc++.h>

#define int long long

const int _ = 1e7;

using namespace std;

bool vis[_ + 7];

int num, prime[_/2], mu[_ + 7];

int n;

void init()
{
    mu[1] = 1;
    for (int i = 2; i <= _; ++i)
    {
        if (!vis[i])
            prime[++num] = i, mu[i] = -1;
        for (int j = 1; j <= num; ++j)
        {
            if (prime[j] * i > _)
                break;
            vis[prime[j] * i] = 1;
            if (i % prime[j] == 0)
            {
                mu[prime[j] * i] = 0;
                break;
            }
            mu[prime[j] * i] = -mu[i];
        }
    }
}

int Ans;

signed main()
{
    init();
    scanf("%lld", &n);
    if (n < 2)
    {
        puts("0");
        return 0;
    }
    for (int x = 1; x <= num; ++x)
    {
        int i = prime[x];
        if (i > n)
            break;
        int ans = 0;
        for (int j = i; j <= n; j += i)
        {
            if (n / j >= 2)
                ans += mu[j / i] * ((n / j) * (n / j - 1) / 2);
        }
        Ans += ans * 2 + 1;
    }
    printf("%lld\n", Ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值