首页 > 其他分享 >[COCI2015-2016#6] PAROVI | 互质覆盖 题解

[COCI2015-2016#6] PAROVI | 互质覆盖 题解

时间:2024-11-21 21:45:13浏览次数:1  
标签:false int 题解 COCI2015 1000000007 tail 10000 互质 线段

前言

不能在同一个坑上栽第三次!

题目链接:原题加强版

题意简述

\(1 \sim n\) 数轴,你可以使用若干条线段 \([l, r]\) 来覆盖,其中要满足 \(\gcd(l, r) = 1\)。问你能够完全覆盖数轴的方案数,对 \(M\) 取模。

\(2 \leq n \leq 10^4\),\(2 \leq M \leq 10^9+7\)。不保证 \(M\) 为质数。

有趣的事实

正解可以做到 \(\mathcal{O}(n^2)\),尽管原题 \(n \leq 20\)。原题固定模数,这里动态模数为了卡打表。

题目分析

我们有理论 \(\mathcal{O}(n^2 \log n)\) 的暴力找出 \(n\) 以内互质的数对,但是太劣了。

可以证明,值域在 \(n\) 以内,互质的数对个数为 \(\dfrac{3}{\pi^2}n^2\) 级别,我还以为不多呢

我们知道 \(\varphi(n) \sim \dfrac{n}{\zeta(2)}\),但是作者太菜了,不会证明。所以数对个数为 \(\sum \limits _ {i = 1} ^ n \varphi(i) = \Theta\Big(\dfrac{n^2}{\zeta(2)}\Big)\)。

据说欧拉又解决了 \(\zeta(2) = \dfrac{\pi^2}{6}\),但是作者还是太菜了,不会证明。所以综合考虑高斯求和里 \(\frac{1}{2}\) 的常数,互质的数对个数为 \(\dfrac{3}{\pi^2}n^2\) 级别。

明明只有 \(\mathcal{O}(n^2)\) 个数对,我们能不能减少冗余计算?答案是肯定的。我们考虑辗转相减法求 \(\gcd\) 的逆过程,从 \((1, 1)\) 开始,每次从 \((a, b)\) 走向 \((a, a+b), (b, a+b)\),可以证明,这样能够不重不漏找到所有互质对。注意 \((1, 1)\) 可能重复统计的细节。

#include <cstdio>
#include <utility>

const int N = 10010;

int n, head = 1, tail;
std::pair<int, int> Q[N * N];

signed main() {
    n = 10000;
    Q[++tail] = { 1, 1 }, ++head;
    Q[++tail] = { 1, 2 };
    while (head <= tail) {
        auto &[x, y] = Q[head++];
        if (x + y > n) continue;
        Q[++tail] = { y, x + y };
        Q[++tail] = { x, x + y };
    }
    printf("%d", tail);
    return 0;
}

于是,从 \(3.75\) 秒优化至 \(0.16\) 秒,我们找出了所有可供使用的线段。下一步就是考虑用这些线段计算答案了。

计算肯定是通过 DP 的方式。阶段显然是当前决策到了第几条线段,但是状态是什么?也就是说怎么刻画一个局面?使用「已经完全覆盖了 \(1 \sim j\)」作为状态合适吗?不太好思考,我们不妨从别的角度来思考问题。

对于线段覆盖类问题,我们一般按照某一端点进行排序,左右端点是本质相同的,这里我们不妨按照左端点从小到大考虑。这对我们的状态设计有什么帮助呢?我们发现,当我们按照左端点排过序后决策,一个最终可能合法的局面,在任意时刻都覆盖了数轴的一段连续前缀。这样考虑,如果当前选择了某一条线段,和上一次之间留有空隙,那么这个空隙在之后都不会被填上,因为之后的线段的左端点不小于当前线段。所以可以使用这种状态。

状态设计好后,转移方程应该信手拈来了。我们再明确一下,记 \(f_{i,j}\) 表示决策了前 \(i\) 条线段,覆盖了数轴 \([1, j]\) 的前缀,方案数为多少。从 \(f_{i-1,j'}\) 转移到 \(f_{i,j}\),若不选择当前线段 \([l_i,r_i]\),则继承 \(i - 1\) 的方案数,即初始 \(f_{i,j}=f_{i-1,j}\),接下来考虑选择这条线段的转移。

  1. \(j' < l_i\)。
    正如上文所言,不能选择当前线段。
  2. \(j' \in [l_i, r_i)\):
    拼上当前线段后,覆盖的前缀增长为 \([1, r_i]\),故 \(f_{i,r_i} \gets f_{i,r_i}+\sum\limits_{j=l_i}^{r_i-1}f_{i-1,j}\)。
  3. \(j' \geq r_i\):
    当前线段选择后,不会改变覆盖的前缀,故 \(f_{i,j}\gets f_{i,j}+f_{i-1,j}\)。

边界 \(f_{0,1} = 1\),答案为 \(f_{k, n}\),其中 \(k = \sum\limits_{i=1}^n\varphi(i)\),即线段条数。

已经能够单次 \(\mathcal{O}(n)\) 转移了,但是这样时间复杂度是 \(\mathcal{O}(n^3)\) 的。滚一滚不做赘述,我们来观察以下代码:

for (int i = 1; i <= n; ++i) {
    for (int j = i + 1; j <= n; ++j)
        if (cop[i][j]) {  // i and j are coprimes
            for (int o = j; o <= n; ++o) f[o] = add(f[o], f[o]);
            for (int o = i; o < j; ++o) f[j] = add(f[j], f[o]);
        }
}

我们发现,这就是一个后缀区间乘 \(2\)、区间求和、单点加操作。有人的 DNA 已经动了,开始线段树模式了。千万不要数据结构学傻了,这么优美的形式,当然要使用优雅地解决方法呀。

倘若不看别的,但看这个乘 \(2\) 操作,我们完全可以一次性扫一遍,只需要扫的时候,维护当前需要乘多少次 \(2\) 即可。

但看这个区间求和,也可以线性扫一遍,同时维护 \([i, j)\) 的 \(f\) 的和即可。

两者,显然可以融合。唯一要小心处理的是 \(f_j\) 先自乘 \(2\),再加。维护方法很多,给出我的方式,可能有些笨:

for (int i = 1; i <= n; ++i) {
    int sum = 0;
    for (int j = n; j >= i; --j) toadd(sum, f[j]);
    for (int j = n; j > i; --j) {
        tosub(sum, f[j]);
        if (cop[i][j])
            toadd(f[j], f[j]), toadd(f[j], sum);
    }
    for (int j = i + 1, tag = 1; j <= n; ++j) {
        tomul(f[j], tag);
        if (cop[i][j]) toadd(tag, tag);
    }
}

于是,我们可以以严格 \(\mathcal{O}(n^2)\) 的时间复杂度解决此问题。

代码

#include <cstdio>
#include <utility>

const short N = 10010;

short n;
int mod;

int head = 1, tail;
std::pair<short, short> Q[N * N];
bool cop[N][N];

int f[N];

inline void toadd(int& a, const int& b) {
    (a += b) >= mod && (a -= mod);
}
inline void tosub(int& a, const int& b) {
    (a -= b) < 0 && (a += mod);
}
inline void tomul(int& a, const int& b) {
    a = 1ll * a * b % mod;
}

signed main() {
    scanf("%hd%d", &n, &mod);
    Q[++tail] = { 1, 1 }, ++head;
    Q[++tail] = { 1, 2 };
    while (head <= tail) {
        auto &[x, y] = Q[head++];
        if (x + y > n) continue;
        Q[++tail] = { x, x + y };
        Q[++tail] = { y, x + y };
    }
    for (int i = 1; i <= tail; ++i)
        cop[Q[i].first][Q[i].second] = true;
    f[1] = 1;
    for (int i = 1; i <= n; ++i) {
        int sum = 0;
        for (int j = n; j >= i; --j) toadd(sum, f[j]);
        for (int j = n; j > i; --j) {
            tosub(sum, f[j]);
            if (cop[i][j])
                toadd(f[j], f[j]), toadd(f[j], sum);
        }
        for (int j = i + 1, tag = 1; j <= n; ++j) {
            tomul(f[j], tag);
            if (cop[i][j]) toadd(tag, tag);
        }
    }
    printf("%d", f[n]);
    return 0;
}
Generator
if (argc < 4) {
	cout << "Usage: " << args[0] << " NMAX MODMAX FORCEMODE SEED" << endl;
	return 0;
}

if (string(args[3]) != "true" && string(args[3]) != "false") {
	cout << "FORCEMODE should be 'true' or 'false'" << endl;
	return 0;
}

int NMAX = stoi(string(args[1]));
int MODMAX = stoi(string(args[2]));
bool FORCEMODE = string(args[3]) == "true";
int SEED = stoi(string(args[4]));

mt19937 rand_num(SEED);

static auto rand = [&] (int l, int r) -> int {
	uniform_int_distribution<int> dist(l, r);
	return dist(rand_num);
};

cout << (FORCEMODE ? NMAX : rand(max(2, NMAX - 234), NMAX)) << ' ' << (FORCEMODE ? MODMAX : rand(max(2, MODMAX - 234), MODMAX)) << endl;
数据范围
const int NMAX[50] = {
	5,     8,     13,    18,    20,
	50,    60,    80,    90,    100,
	500,   500,   500,   500,   500,   500,   500,   500,   500,   500,
	1000,  1000,  1000,  1000,  1000,  1000,  1000,  1000,  1000,  1000,
	5000,  5000,  5000,  5000,  5000,  5000,  5000,  5000,  5000,  5000,
	10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000
};
const int MODMAX[50] = {
	1000000000, 1000000000, 1000000000, 1000000000, 1000000000,
	2,          20,         200,        2000,       20000,
	114,        514,        19,         19,         810,        10000,      10000,      10000,      10000,      10000,
	100000,     100000,     1000000,    10000000,   100000000,  1000000007, 1000000007, 1000000007, 1000000007, 1000000007,
	1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007,
	1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007, 1000000007,
};
const bool FORCEMODE[50] = {
	true,  true,  true,  true,  true,
	true,  true,  true,  true,  true,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, false,
	false, false, false, false, false, false, false, false, false, true
};

反思

线段覆盖类问题,这种处理方法是套路的。做完这题可以尝试 [USACO20FEB] Help Yourself P

标签:false,int,题解,COCI2015,1000000007,tail,10000,互质,线段
From: https://www.cnblogs.com/XuYueming/p/18559166

相关文章

  • 【题解】AT_joisc2007_mall ショッピングモール (Mall)
    原题传送门温馨提示:岛国题要换行!需要求一个矩阵的和,考虑二维前缀和。题目中不允许矩阵中有负数,结合求和的最小值,我们把负数赋为最大值不就行了吗。接下来就是求二维前缀和了。基于容斥原理,二维前缀和有如下递推关系:\[sum_{i,j}=sum_{i-1,j}+sum_{i,j-1}-sum_{i-1,j-1}+c_{i......
  • 【题解】AT_agc011_b [AGC011B] Colorful Creatures
    原题传送门我们知道,要想使一个生物能活到最后,那么它进行的每一次吸收前,它的大小应当尽可能大,所以我们考虑贪心,对生物的大小从小到大排序,每个生物都从小的开始吸收,看能不能活到最后,时间复杂度\(\mathcal{O(n^2)}\)。我们还知道,排序后,生物\(i\)能活到最后,则生物\(i+1\simn\)......
  • P7906 [Ynoi2005] rpxleqxq 题解
    P7906[Ynoi2005]rpxleqxq题解题目大意给定一个长度为\(n\)的序列\(A\),和一个常数\(k\)。有\(m\)次询问,每次给定一个区间\([l,r]\),询问有多少二元组\((i,j)\),满足:\(1\leqi<j\leqn\);\((A_i\oplusA_j)\leqk\)。Solve前置知识:莫队二次离线。对于普通莫队,端......
  • [CSP-S2019]Emiya 家今天的饭 题解
    题意分析给出一个矩阵,要求每行只能选一个节点,每列选的节点不能超过所有选的节点的一半,不能不选,给出每个节点的选择方案数,求总方案数考场思路考虑暴力枚举每一个点的选择情况,最后统计答案。对于行:但是因为有每一行只能选择一个的限制,所以考虑当前行选择一个后直接转跳到下一行......
  • [NOIP2016 提高组] 蚯蚓 题解
    考场思路考虑要动态维护最大值,可以直接使用优先队列进行维护,但是,考虑到我们并不好直接修改优先队列中的每一个元素,所以决定使用vector先排一遍序,再使用冒泡排序进行动态维护,时间复杂度\(O(mn)\),可以拿35pts。代码#include<iostream>#include<vector>#include<algorithm>......
  • C语言 蓝桥杯某例题解决方案(查找完数)
    蓝桥杯原题: 一个数如果恰好等于它的因子之和,这个数就称为“完数”。例如6=1+2+3.编程找出1000以内的所有完数。这个题没有很大的难点,与我们上一个解决的问题“质因数分解”不同,它不需要判断因数是否是质数,因此我们的工作量会小很多。现在我们的想法还是类似,首先找到......
  • 【Flinkcdc问题解决】java.lang.NoClassDefFoundError: org/apache/flink/shaded/guav
    1.环境介绍Flink1.17+Flinkcdc2.2.12.问题描述使用Flink1.17和Flinkcdc2.2.1环境进行数据加工,但是报以上错误,原因是版本不匹配,flinkcdc2.2.1用的是guava18,但是flink1.17用的是guava30,导致冲突。3.问题解决添加flink-sql-connector-mysql-cdc依赖<dependen......
  • [题解](更新中)2024/11/21 模拟赛 / 2023牛客OI赛前集训营-提高组(第二场) A~B
    整套都是原题所以就不设密码了(原题页面:https://ac.nowcoder.com/acm/contest/65193题解:https://www.nowcoder.com/discuss/540225827162583040\(60+30+20+20=130\)。每日挂分之T2线段树不开\(4\)倍+\(10^6\)数量级输入不关同步流,\(\bf\colorbox{MidnightBlue}{\texttt{\color{......
  • ABC379 题解[A-D]
    ABC379题解目录ABC379题解目录A CyclicB StrawberriesC SowingStonesD HomeGardenE SumofAllSubstringsA Cyclicmanwhatcanisay?#include<bits/stdc++.h>usingnamespacestd;usingll=longlong;usingull=unsignedlonglong;usingld=l......
  • 去水印小程序downloadFile域名问题解决方式
    ......