├── 010~019 ├── 011【页面】目录.md ├── 017【算法】最大公约数、最小公倍数.md ├── 018【算法】快速幂.md ├── 013【数论】模逆元.md ├── 010【题目】满意的集合.md └── 012【题目】全体集合.md ├── 170~179 ├── 172【杂项】新的旅途.md ├── 177【机器学习】残差神经网络.md └── 175【机器学习】卷积.md ├── 001~009 ├── 001【杂项】Hello World!.md ├── 002【页面】关于.md ├── 009【题目】木棍游戏.md ├── 007【算法】二分查找、三分查找.md ├── 008【算法】深度优先搜索、广度优先搜索.md ├── 005【算法】埃氏筛、欧拉筛.md └── 006【数据结构】单调队列、单调栈.md ├── README.md ├── 060~069 ├── 065【算法】最短路径算法归纳.md ├── 061【数据结构】字典树.md ├── 064【算法】Bellman-Ford 算法.md ├── 067【数论】扩展欧几里得算法.md ├── 066【数论】欧拉函数.md └── 063【数据结构】哈希表.md ├── 020~029 ├── 028【算法】Floyd-Warshall 算法.md ├── 025【数论】算术基本定理.md ├── 021【题目】digitnum .md ├── 029【算法】SPFA (最短路径快速算法).md ├── 023【题目】Range Sums.md ├── 024【题目】XOR to All.md ├── 022【题目】AND and SUM.md └── 027【算法】Dijkstra 算法.md ├── 070~079 ├── 071【数论】容斥原理.md ├── 077【题目】青蛙的约会.md ├── 079【题目】序列统计.md ├── 073【题目】Permutation Operations.md └── 072【题目】Orray.md ├── 050~059 ├── 058【算法】前缀和、差分.md ├── 054【数据结构】树状数组.md ├── 052【题目】Distance Sequence.md ├── 050【题目】Prefix Equality.md ├── 059【题目】I Hate Non-integer Number.md ├── 055【题目】Gift Tax.md ├── 057【算法】高精度运算.md └── 056【题目】K Derangement.md ├── 110~119 ├── 116【数据结构】ST 表.md └── 114【题目】Matches.md ├── 130~139 └── 137【数据结构】Kruskal 重构树.md ├── 100~109 ├── 100【算法】矩阵加速算法.md └── 103【算法】最长上升子序列.md ├── 180~189 ├── 187【机器学习】简单偏好优化 (SimPO).md └── 188【机器学习】组相关策略优化 (GRPO).md ├── 160~169 ├── 160【题目】维克多词典.md └── 164【数据结构】归并树.md ├── 140~149 ├── 141【题目】Insert 1, Insert 2, Insert 3, ....md ├── 148【题目】Agnej.md └── 149【题目】IUPC.md ├── 120~129 ├── 127【题目】Nazrin the Greeeeeedy Mouse.md ├── 126【题目】Qu'est-ce Que C'est.md ├── 129【题目】Circle of Mistery.md ├── 123【算法】最近公共祖先.md └── 128【题目】Merge the squares.md ├── 030~039 ├── 035【题目】1111gal password.md ├── 032【算法】Kruskal 算法.md ├── 034【算法】Prim 算法.md └── 036【题目】(∀x∀) .md ├── 150~159 ├── 155【题目】雪菜的式子.md ├── 158【题目】新取模运算.md └── 157【题目】简单的加法乘法计算题.md ├── 080~089 ├── 084【题目】最短汉密尔顿路径.md ├── 082【题目】Factorial and Multiple.md └── 086【题目】Hossam and Friends.md ├── 040~049 ├── 041【题目】2-variable Function.md ├── 048【题目】Ignore Operations.md └── 045【题目】K-colinear Line.md └── 090~099 └── 099【题目】Geometric Progression.md /010~019/011【页面】目录.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /170~179/172【杂项】新的旅途.md: -------------------------------------------------------------------------------- 1 | 大一 ~ 大三上 短暂两年半 ACM 生涯落下帷幕,虽说结果不如人意但是过程收获颇丰,每一次比赛都令人难忘。 2 | 3 | 这两年半花了很多时间练习 ACM 算法,以至于轻视了人工智能专业内机器学习的内容。今后本博客可能不会再涉猎 ACM 内容,而是转为其他方面的笔记分析,大概率是机器学习相关的笔记。 4 | 5 | 还要感谢每个在 GitHub 上点 Star 的同学,居然让我的笔记成为了我目前 Star 最多的仓库,非常感谢你们的支持!同时也很抱歉,可能今后并不会再更新你们感兴趣的 ACM 内容了。 6 | 7 | 旧的旅途已经到达终点,接下来踏向新的目的地吧! -------------------------------------------------------------------------------- /001~009/001【杂项】Hello World!.md: -------------------------------------------------------------------------------- 1 | **欢迎访问我的博客!** 2 | 3 | 本站为颢天博客的技术分站,主要分享自己的学习笔记,技术性较强并且枯燥无聊。想着丢在主站除了浪费位置就没啥其他用处,于是专门新开一个博客来写。 4 | 5 | 6 | 7 | 与主站不同,本站使用的博客引擎为 Typecho,相较于 WordPress 更加轻量级,访问速度非常的快(对比了下才感觉 WordPress 磨磨唧唧的,真是拖沓)而且我可以直接写 MarkDown 然后复制进去,更适合作为学习笔记。 8 | 9 | 虽说没打算有啥人看我的笔记,并且我如今的能力非常有限,但如果我的文章可以帮助到你,我还是非常高兴哒! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haotian-BiJi 2 | 3 | [颢天笔记](https://io.zouht.com/) 网站的 Markdown 源文件,内容大多为 ACM 竞赛算法与竞赛题目题解,目前正在向机器学习转型。 4 | 5 | **命名规则:** 6 | 7 | `CID`【`Category`】`Title`.md 8 | 9 | - `CID` - 对应网站 Typecho 的页面 ID. 10 | - `Category` - 对应文章的分类目录,目前有算法、数据结构、数论、题目、杂项。 11 | - `Title` - 对应文章标题。 12 | 13 | **关于公式:** 14 | 15 | GitHub 网页已经支持 $\LaTeX$ 公式渲染,直接打开即可查看,GitHub 移动端 APP 暂不支持。 16 | 17 | 由于 GitHub 中标记前后均空格时才会识别为公式,因此有些公式没有被正确识别并渲染。 18 | 19 | 最佳浏览方式是前往我的网站或使用支持 $\LaTeX$ 的本地 Markdown 编辑器。 20 | 21 | **许可协议:** 22 | 23 | 本作品采用[知识共享署名-相同方式共享 4.0 国际许可协议](http://creativecommons.org/licenses/by-sa/4.0/)进行许可。 24 | 25 | This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/). 26 | 27 | -------------------------------------------------------------------------------- /001~009/002【页面】关于.md: -------------------------------------------------------------------------------- 1 | > **转载须知** 2 | > 若无特别说明,本站所有文章使用:[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh) 许可,转载时请遵守协议规范 3 | > 文章 Markdown 源文件:https://github.com/ChrisKimZHT/Haotian-BiJi 4 | 5 | ## 网站简介 6 | 7 | 本站为[颢天博客](https://www.zouht.com/)的分站——颢天笔记,主要分享自己的学习笔记,目前的内容主要是一些算法题。 8 | 9 | 本站与主站的最大差别就是,主站有许多通俗易懂的内容,涉及面比较广,许多教程非常详尽,本站专业性较强,非常枯燥无味。如果你并不是为了专业内容而来,可以关掉这个网页了。 10 | 11 | 目前我深知自己的能力还很有限,还是一位算法菜鸟,目前笔记内容还处于基础等级,今后会不断补充修改笔记。 12 | 13 | 如果有大佬对文章有意见,也欢迎在评论区批评指正。 14 | 15 | ## 网站信息 16 | 17 | 建站时间2021年12月12日! 18 | 19 | 由 Typecho 强力驱动,默认主题。 20 | 21 | 服务器使用阿里云上海ESC,规格:ecs.t6-c1m2.large (性能约束实例) 22 | 23 | 本站文章采用 MarkDown 编写,公式使用 $LaTeX$ 编写(MathJax 驱动) 24 | 25 | 本站图片采用 WebP 格式压缩,旧浏览器内核无法显示 WebP 格式。 26 | 27 | ## 个人信息 28 | 29 | 请前往我的主页查看:https://www.chriskim.cn/ -------------------------------------------------------------------------------- /010~019/017【算法】最大公约数、最小公倍数.md: -------------------------------------------------------------------------------- 1 | **计算两非负整数最大公约数:**辗转相除法(欧几里得算法) 2 | 3 | **计算两非负整数最小公倍数:**通过这两个数的最大公约数间接求得 4 | 5 | 6 | 7 | # 最大公约数 8 | 9 | ## 辗转相除法 (欧几里得算法, Euclidean algorithm) 10 | 11 | **依赖定理:** 12 | 13 | 两个整数的最大公约数等于其中较小的那个数和两数相除余数的最大公约数。即: 14 | $$ 15 | \gcd(a,b)=\gcd(b,a\bmod b)\ (a>b,且r=a\bmod b,r\neq 0) 16 | $$ 17 | **简单证明:** 18 | 19 | $a$ 可表示为 $a=kb+r$ $(r\neq0)$ 20 | 21 | 假设 $\gcd(a,b)=d$,即 $d|a$ 且 $d|b$ 22 | 23 | $r=a-kb$,由 $d|a$ 且 $d|b$ 可知 $d|r$ 24 | 25 | 由 $r 4 | 5 | # 单源汇最短路径问题 6 | 7 | 这种问题给出一个起点,求该起点到其他点的最短路径。 8 | 9 | ## 边权为非负数 10 | 11 | ### 稠密图 12 | 13 | 建议使用邻接矩阵储存稠密图 14 | 15 | **朴素版 Dijkstra 算法**:https://io.zouht.com/27.html 16 | 17 | 时间复杂度:$O(\left|V\right|^2)$ 18 | 19 | **其他算法** 20 | 21 | 可处理负边权的算法可兼容非负权边情况、多源汇最短路算法可兼容单源汇情况。 22 | 23 | ### 稀疏图 24 | 25 | 建议使用邻接表储存稀疏图 26 | 27 | **二叉堆优化版 Dijkstra 算法**:https://io.zouht.com/27.html 28 | 29 | 时间复杂度:$O((\left|E\right|+\left|V\right|)\log\left|V\right|)$ 30 | 31 | **其他算法** 32 | 33 | 可处理负边权的算法可兼容非负权边情况、多源汇最短路算法可兼容单源汇情况。 34 | 35 | ## 边权有负数 36 | 37 | **Bellman-Ford 算法**:https://io.zouht.com/64.html 38 | 39 | 时间复杂度:$O(\left|V\right|\cdot\left|E\right|)$ 40 | 41 | **SPFA**:https://io.zouht.com/29.html 42 | 43 | 平均时间复杂度:$O(\left|E\right|)$ 44 | 45 | 最差时间复杂度:$O(\left|V\right|\cdot\left|E\right|)$ 46 | 47 | # 多源汇最短路径问题 48 | 49 | 这种问题需要求任意起点到任意终点的最短路径。 50 | 51 | **Floyd-Warshall 算法**:https://io.zouht.com/28.html 52 | 53 | 时间复杂度:$O(\left|V\right|^3)$ 54 | -------------------------------------------------------------------------------- /020~029/028【算法】Floyd-Warshall 算法.md: -------------------------------------------------------------------------------- 1 | **解决赋权图的多源最短路径问题:**Floyd-Warshall 算法 2 | 3 | - 能解决负边 4 | - 不能解决负环 5 | 6 | 7 | 8 | # Floyd-Warshall 算法 9 | 10 | ## 复杂度 11 | 12 | 时间复杂度:$O(\left|V\right|^3)$ 13 | 14 | $\left|V\right|$ 为顶点数 15 | 16 | ## 分析 17 | 18 | Floyd 算法比较简单,即暴力枚举了所有可能,从而找到任意两点的距离。 19 | 20 | 从点 $i$ 到点 $j$ 的走法无非两种: 21 | 22 | - $i\rightarrow j$ 23 | - $i\rightarrow k\rightarrow j$ 24 | 25 | 那么三个循环,遍历所有的 $i,j,k$,如果 $i\rightarrow k\rightarrow j$ 距离比 $i\rightarrow j$ 短,那么就更新距离。 26 | 27 | #### 防止无穷大溢出 28 | 29 | 数学中 $\infty+\infty=\infty$,但程序中两个代表 $\infty$ 的大数相加肯定是会溢出成负数的,这会对该松弛操作产生巨大的干扰。因此进行松弛操作时需要判断两数是否为 $\infty$,只要有一个数为 $\infty$,就跳过这次松弛操作以防溢出。 30 | 31 | ## 代码实现 32 | 33 | ```cpp 34 | const int MAXN = 1010, INF = 0x3f3f3f3f; 35 | int e, v; // e边数 v顶点数 36 | int dist[MAXN][MAXN]; // dist[x][y]代表x到y的距离 37 | 38 | void floyd_init(void) 39 | { 40 | memset(dist, 0x3f, sizeof(dist)); 41 | for (int i = 1; i <= v; i++) 42 | dist[i][i] = 0; 43 | } 44 | 45 | void floyd(void) 46 | { 47 | for (int k = 1; k <= v; k++) 48 | for (int i = 1; i <= v; i++) 49 | for (int j = 1; j <= v; j++) 50 | if (dist[i][k] < INF && dist[k][j] < INF) 51 | dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /060~069/061【数据结构】字典树.md: -------------------------------------------------------------------------------- 1 | **字典树** (单词查找树, Trie, /ˈtraɪ/): 是一种有序树,用于保存关联数组,其中的键通常是字符串。 2 | 3 | 4 | 5 | # 字典树 6 | 7 | ### 时间复杂度:$O(m)$ 8 | 9 | $m$ 为待操作字符串的长度。 10 | 11 | ### 分析 12 | 13 | 储存多个字符串的朴素方法就是将字符串储存到多个字符数组内,但这样储存比较占用空间,并且查询操作比较费时。 14 | 15 | 如果字符串的字符种类较少,例如只有小写字母、只有大写字母、只有数字、只有 0/1 这种情况,就可以使用字典树这一种数据结构(也可以使用二进制转化为 0/1 的情况)。 16 | 17 | 18 | 19 | (图片来源:[Wikipedia](https://en.wikipedia.org/wiki/File:Trie_example.svg),属于公有领域) 20 | 21 | 实现方式,与链表比较类似。使用了一个二维数组储存子节点指针,第一维代表节点序号,其中 $0$ 号节点代表头节点,也代表空节点,节点结尾指向空节点。第二维代表字母,若储存小写字母,则需要 $26$ 个,$0\sim25$ 分别代表 $a\sim z$. 22 | 23 | ### 代码实现 24 | 25 | (以下代码示例储存的为小写字母) 26 | 27 | ```cpp 28 | const int MAXN = 1e6 + 10; 29 | int son[MAXN][26], cnt[MAXN], idx; 30 | ``` 31 | 32 | **插入** 33 | 34 | ```cpp 35 | void insert(string &s) 36 | { 37 | int p = 0; 38 | for (int i = 0; i < s.size(); i++) 39 | { 40 | int c = s[i] - 'a'; 41 | if (!son[p][c]) 42 | son[p][c] = ++idx; 43 | p = son[p][c]; 44 | } 45 | cnt[p]++; 46 | } 47 | ``` 48 | 49 | **查询** 50 | 51 | 返回待查询字符串的数量。 52 | 53 | ```cpp 54 | int query(string &s) 55 | { 56 | int p = 0; 57 | for (int i = 0; i < s.size(); i++) 58 | { 59 | int c = s[i] - 'a'; 60 | if (!son[p][c]) 61 | return 0; 62 | p = son[p][c]; 63 | } 64 | return cnt[p]; 65 | } 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /010~019/018【算法】快速幂.md: -------------------------------------------------------------------------------- 1 | **提升幂运算速度的算法:**快速幂 2 | 3 | 4 | 5 | # 快速幂 6 | 7 | ## 复杂度 8 | 9 | 时间复杂度:$O(\log_2n)$ 10 | 11 | ($n$ 为幂数) 12 | 13 | ## 代码实现 14 | 15 | ```cpp 16 | long long fastpow(long long a, long long b) 17 | { 18 | long long ans = 1; 19 | while (b) 20 | { 21 | if (b % 2 == 1) 22 | ans = ans * a; 23 | a = a * a; 24 | b >>= 1; 25 | } 26 | return ans; 27 | } 28 | ``` 29 | 30 | ## 算法思路 31 | 32 | $2^{32}=4^{16}=16^{8}=256^{4}=65536^{2}=4294967296$ 33 | 34 | 如果用一般的循环算法,$2^{32}$ 需要循环 32 次,而通过上面的方法,我们只用了 $\log_{2}32=5$ 次循环,就得出了答案,这指数较小时差别不大,但指数非常大时差别会很大。 35 | 36 | 当然实际情况不会像上面那么顺利,指数不一定能够一直为偶数,所以需要一些额外操作。 37 | 38 | 以 $3^{11}$ 为例: 39 | 40 | 指数为奇数 $11$,单独分出一个底数 $3$,即 $3^{11}=3\times 3^{10}$,把分出的底数 $3$ 乘入 $ans$,现在 $ans = 3$ 41 | 42 | 指数为偶数 $10$,可以将指数除二,即 $3^{10}=9^{5}$,现在底数为 $9$,指数为 $5$ 43 | 44 | 指数为奇数 $5$,单独分出一个底数 $9$,即 $9^{5}=9\times9^{4}$,把分出的底数 $9$ 乘入 $ans$,现在 $ans = 27$ 45 | 46 | 指数为偶数 $4$,可以将指数除二,即 $9^{4}=81^{2}$,现在底数为 $81$,指数为 $2$ 47 | 48 | 指数为偶数 $2$,可以将指数除二,即 $81^{2}=6561^{1}$,现在底数为 $6561$,指数为 $1$ 49 | 50 | 指数为奇数 $1$,单独分出一个底数 $6561$(实际上就是全部),把分出的底数 $6561$ 乘入 $ans$,现在 $ans = 177147$ 51 | 52 | 指数为 $0$,循环结束,得到 $3^{11}=177147$ 53 | 54 | 在代码中,实际上分出一个底数后,并没有特意将指数减一,而是直接继续操作,原因是之后除二后,整型被截断,一样能得到正确结果,例如上面的 11 >> 1 = 5, 5 >> 1 = 2(整型右移其实和除二一样) 55 | 56 | ## 大数取模 57 | 58 | 一般能用到快速幂算法的,答案都会非常巨大,肯定是超出已有数据类型范围的,所以题目一般会说答案对某个数字取模。快速幂可以完成这个操作,只需要进行很小的修改。 59 | 60 | ```cpp 61 | const long long MOD = 20220128; 62 | long long fastpow(long long a, long long b) 63 | { 64 | a %= MOD; // 开头先取一次模 65 | long long ans = 1; 66 | while (b) 67 | { 68 | if (b % 2 == 1) 69 | ans = ans * a % MOD; // 每次运算都取模 70 | a = a * a % MOD; // 每次运算都取模 71 | b >>= 1; 72 | } 73 | return ans; 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /070~079/071【数论】容斥原理.md: -------------------------------------------------------------------------------- 1 | **容斥原理**:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。 2 | 3 | 4 | 5 | # 容斥原理 6 | 7 | ## 描述 8 | 9 | 若 $A_1,\dots,A_n$ 为有限集,则 10 | $$ 11 | {\displaystyle {\begin{aligned}\left|\bigcup _{i=1}^{n}A_{i}\right|={}&\sum _{i=1}^{n}|A_{i}|-\sum _{1\leq i 20 | 21 | (图片和公式来源:[维基百科](https://zh.wikipedia.org/wiki/%E6%8E%92%E5%AE%B9%E5%8E%9F%E7%90%86),使用 [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/deed.zh) 协议) 22 | 23 | # 代码实现 24 | 25 | 在容斥原理中,我们枚举了 $A_1,\dots,A_n$ 的所有选择情况,一共 $2^n$ 种情况。我们可以使用 DFS 进行遍历,但通常我们使用二进制的方法进行遍历会更方便。 26 | 27 | 我们可以将每种选择情况对应到一个 $n$ 位的二进制数上,例如:选 $A_2$ 对应 $\overbrace{0\dots0010}^{n位}$,选 $A_1,A_3$ 对应 $\overbrace{0\dots0101}^{n位}$. 那么我们从 $1$ 遍历到 $2^n-1$ 即可将(除不选的)所有选择情况全部遍历到。如果可以全不选的话,从 $0$ 开始遍历。 28 | 29 | 那么我们怎么得知某一位是否为 $1$ 呢?使用位运算,`i >> n & 1` 如果为 $1$ 那么就说明 $i$ 的第 $n$ 位为 $1$. 例如: 30 | $$ 31 | \begin{align} 32 | i &=0001\ 0010\ 0100\ 0011\\ 33 | i\gg6 &=0000\ 0000\ 0100\ 1001\\ 34 | 1 &=0000\ 0000\ 0000\ 0001\\ 35 | i\gg6\ \&\ 1&=0000\ 0000\ 0000\ 0001\\ 36 | \end{align} 37 | $$ 38 | 39 | ```cpp 40 | for (int i = 1; i < (1 << n); i++) 41 | for (int j = 0; j < n; j++) 42 | if (i >> j & 1) 43 | ... 44 | ``` 45 | 46 | 实际应用中,往往需要按题目调整位运算的方式,但总体思路仍然是使用二进制数来遍历。 -------------------------------------------------------------------------------- /050~059/058【算法】前缀和、差分.md: -------------------------------------------------------------------------------- 1 | **原数组:**$[3,1,4,1,5,9]$ 2 | **前缀和:**$[3,4,8,9,14,23]$ 3 | **差分:**$[3,-2,3,-3,4,4]$ 4 | 5 | 6 | 7 | # 前缀和 8 | 9 | 通过前缀和数组,可以以 $O(1)$ 的复杂度查询到一个连续区间内元素的和。 10 | 11 | ## 一维 12 | 13 | **构造方式** 14 | $$ 15 | ps[i]=ori[1]+ori[2]+\cdots+ori[i] 16 | $$ 17 | **查询方式** 18 | $$ 19 | ori[l]+\cdots+ori[r]=ps[r]-ps[l-1] 20 | $$ 21 | ![](https://assets.zouht.com/img/note/58-01.webp) 22 | 23 | ## 二维 24 | 25 | **构造方式** 26 | $$ 27 | ps[i][j]=ps[i-1][j]+ps[i][j-1]-ps[i-1][j-1]+ori[i][j] 28 | $$ 29 | ![](https://assets.zouht.com/img/note/58-02.webp) 30 | 31 | **查询方式** 32 | $$ 33 | \begin{align} 34 | &(x_1,y_1)为左上顶点(x_2,y_2)为右下顶点的矩阵的总和\\ 35 | =\ &ps[x_2][y_2]-ps[x_1-1][y_2]-ps[x_2][y_1-1]+ps[x_1-1][y_1-1] 36 | \end{align} 37 | $$ 38 | 原理与上图类似,因此不画图了。 39 | 40 | # 差分 41 | 42 | 通过差分数组,可以以 $O(1)$ 的复杂度向一个连续区间内元素加上一个数。 43 | 44 | ## 一维 45 | 46 | **构造方式** 47 | $$ 48 | df[i]=ori[i]-ori[i-1] 49 | $$ 50 | 或者写成与下面统一的形式: 51 | $$ 52 | \begin{align} 53 | df[i]\ &+\!=ori[i]\\ 54 | df[i+1]\ &-\!=ori[i] 55 | \end{align} 56 | $$ 57 | 58 | 59 | **修改方式** 60 | 61 | 给区间 $[l,r]$ 中每个数 $+c$: 62 | $$ 63 | \begin{align} 64 | df[l]\ &+\!=c\\ 65 | df[r+1]\ &-\!=c 66 | \end{align} 67 | $$ 68 | ![](https://assets.zouht.com/img/note/58-03.webp) 69 | 70 | ## 二维 71 | 72 | **构造方式** 73 | $$ 74 | \begin{align} 75 | df[i][j]\ &+\!=ori[i][j]\\ 76 | df[i+1][j]\ &-\!=ori[i][j]\\ 77 | df[i][j+1]\ &-\!=ori[i][j]\\ 78 | df[i+1][j+1]\ &+\!=ori[i][j] 79 | \end{align} 80 | $$ 81 | ![](https://assets.zouht.com/img/note/58-04.webp) 82 | 83 | **修改方式** 84 | 85 | 给区间 $(x_1,y_1)为左上顶点(x_2,y_2)为右下顶点的矩阵$ 中每个数 $+c$: 86 | $$ 87 | \begin{align} 88 | df[x_1][y_1]\ &+\!=c\\ 89 | df[x_2+1][y_1]\ &-\!=c\\ 90 | df[x_1][y_2+1]\ &-\!=c\\ 91 | df[x_2+1][y_2+1]\ &+\!=c 92 | \end{align} 93 | $$ 94 | 原理与上图类似,因此不画图了。 95 | -------------------------------------------------------------------------------- /010~019/013【数论】模逆元.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | $a,b\in\mathbb{Z}$,且 $ab \equiv 1 \pmod{n}$,则称 $a$ 和 $b$ 关于模 $n$ 互为**模逆元**(Modular Multiplicative Inverse) 4 | 5 | 6 | 7 | 还可记作:$b \equiv \frac{1}{a} \pmod{n}$ 或 $b \equiv a^{-1} \pmod{n}$ 8 | 9 | ## 模运算的部分性质 10 | 11 | $(a+b)\bmod n=(a\bmod n+b\bmod n)\bmod n$ 12 | 13 | $(a-b)\bmod n=(a\bmod n-b\bmod n+n)\bmod n$ 14 | 15 | $(a\cdot b)\bmod n=((a\bmod n)\cdot (b\bmod n))\bmod n$ 16 | 17 | $a^b\bmod n=(a\bmod n)^b\bmod n$ 18 | 19 | 上面三条性质,相当于加、减、乘、乘方都齐了,那么除呢?$\frac{a}{b}\bmod n=\ ?$ 20 | 21 | 这就需要模逆元来解决。 22 | 23 | ## 模逆元 24 | 25 | 类比倒数 $a\cdot b=1$, $ab \equiv 1 \pmod{n}$ 也可以称它们互为模倒数(模逆元) 26 | 27 | 那么如果知道 $b$ 的模逆元为 $x$, $\frac{a}{b}\bmod c$ 就可以转化为 $(a\cdot x) \bmod c$ 28 | 29 | ## 求法 30 | 31 | > **费马小定理 **(Fermat's Little Theorem) 32 | > 33 | > 若 $p$ 为质数,整数 $a$ 与 $p$ 互质,则 $a^{p-1}\equiv 1\pmod p$ 34 | > 35 | > 另一种形式:对于任意整数 $a$,有 $a^p\equiv a\pmod p$ 36 | 37 | 由以上定理: 38 | 39 | $a^{p-1}\equiv 1\pmod p$ 40 | 41 | 左边提出一个 $a$: 42 | 43 | $a\cdot a^{p-2}\equiv 1\pmod p$ 44 | 45 | 因此可得出: 46 | 47 | $a^{-1}\equiv a^{p-2}\pmod p$ 48 | 49 | 因此 $a$ 关于模 $p$ 的逆元就是 $a^{p-2}\bmod p$。 50 | 51 | 例如要求 $(15/5)\bmod 4$,就先求 $5$ 关于 $4$ 的逆元 $5^{4-2}\bmod 4=25\bmod 4=1$,然后就可以转化为 $(15/5)\bmod 4=(15\cdot 1)\bmod 4=3$ 52 | 53 | **注意:** 54 | 55 | - 满足整数 $a$ 与 $p$ 互质,才存在逆元,否则逆元不存在。 56 | - $p$ 为质数才可使用费马小定理计算逆元,否则需要使用[扩展欧几里得算法](https://io.zouht.com/67.html)求逆元。 57 | 58 | ## [快速幂](/18.html) 59 | 60 | 在实际题目中,模 $p$ 往往非常大,比如为 $20220211$,那要求一个数的模逆元就会进行一个很大指数运算,因此为了节省时间需要使用快速幂。同时快速幂也兼容取模,因此可以在算幂的同时进行取模运算。 61 | 62 | ```cpp 63 | #define MOD 20220211 64 | long long fastpow(long long a, long long b) 65 | { 66 | a %= MOD; // 开头先取一次模 67 | long long ans = 1; 68 | while (b) 69 | { 70 | if (b % 2 == 1) 71 | ans = ans * a % MOD; // 每次运算都取模 72 | a = a * a % MOD; // 每次运算都取模 73 | b >>= 1; 74 | } 75 | return ans; 76 | } 77 | ``` -------------------------------------------------------------------------------- /110~119/116【数据结构】ST 表.md: -------------------------------------------------------------------------------- 1 | **ST 表 (Sparse Table):**对于可重复贡献问题,可在 $O(n\log n)$ 完成初始化,在 $O(1)$ 回答每个区间查询的数据结构。但是不支持修改数据。 2 | 3 | **可重复贡献问题:**对于运算 $\star$,如果 $x\star x=x$,且 $\star$ 要满足结合律,则对应的区间询问是可重复贡献问题。符合这个性质的常见运算有 $\max,\min,\gcd$. 4 | 5 | 6 | 7 | # ST 表 8 | 9 | ### 预处理 10 | 11 | 对于一个数列,ST 表储存的便是其二的幂次长度的每一段的计算结果。 12 | 13 | 具体点来说,如果一个数列长度为 $9$,我们用中括号 $[l,r]$ 表示要查询的区间,那么 ST 表储存的便是: 14 | 15 | - 长度为 $1$ 的结果:$[1,1],[2,2],[3,3],[4,4],[5,5],[6,6],[7,7],[8,8],[9,9]$ 16 | - 长度为 $2$ 的结果:$[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9]$ 17 | - 长度为 $4$ 的结果:$[1,4],[2,5],[3,6],[4,7],[5,8],[6,9]$ 18 | - 长度为 $8$ 的结果:$[1,8],[2,9]$ 19 | 20 | 将区间可视化的话如下图: 21 | 22 | ![](https://assets.zouht.com/img/note/116-01.webp) 23 | 24 | 对于长度为 $n$ 的数列,将要计算 $\log n$ 个不同长度的结果,每种长度结果的数量级为 $n$. 那么预处理的时间复杂度为:$O(n\log n)$ 25 | 26 | 预处理数据储存在一个二维数组 $f$ 内,第一维为区间长度,第二维为区间起始下标: 27 | 28 | - 首先读入数列,储存到长度为 $1$ 的结果内 29 | - 然后通过上一层的结果递推本层结果。即:对于 $2^k$ 长度的结果,使用 $2^{k-1}$ 的结果进行计算。转移方程为($\star$ 代表具体场景的运算):$f_{i,j}=\star(f_{i-1,j},f_{i-1,j+2^{i-1}})$ 30 | 31 | ### 求解 32 | 33 | 由于 ST 表解决的是可重复贡献问题,那么两个有重复部分的区间的进行计算的话,并不会影响答案的正确性。 34 | 35 | 具体点来说,如果我们要求一个数列的区间 $[2,8]$ 的最大值,那么我们对 $[2,6]$ 的最大值和 $[4,8]$ 的最大值取最大值,就能得到 $[2,8]$ 的最大值。 36 | 37 | 对于任意一个区间 $[l,r]$,我们都可以分解为上面预处理出来的两个区间,具体分解方式如下: 38 | 39 | - 记区间长度为 $d=r-l+1$ 40 | - 分解出来的两个区间的长度为 $d'=2^{\lfloor\log_2 d\rfloor}$ (相当于找到 $\leq d$ 的最大的 $2$ 的幂次) 41 | - 分解出来的区间为:$[l,l+(d'-1)]$ 和 $[r-(d'-1),r]$ 42 | 43 | 将区间分解好后,我们就用上面预处理得到的结果直接计算出答案。 44 | 45 | 因此可以在 $O(1)$ 取得任意一个区间的结果。 46 | 47 | ### 模板 48 | 49 | 运算为 $\max$,数列长度为 $N$. 50 | 51 | **预处理** 52 | 53 | ```cpp 54 | void init() 55 | { 56 | for (int i = 1; i <= N; i++) 57 | cin >> f[0][i]; 58 | for (int i = 1; i <= LOGN; i++) 59 | for (int j = 1; j <= N - (1 << i) + 1; j++) 60 | f[i][j] = max(f[i - 1][j], f[i - 1][j + (1 << (i - 1))]); 61 | } 62 | ``` 63 | 64 | **查询** 65 | 66 | ```cpp 67 | int query(int l, int r) 68 | { 69 | int len = r - l + 1; 70 | int pow = 0, pow2 = 1; 71 | while (pow2 * 2 <= len) 72 | { 73 | pow++; 74 | pow2 *= 2; 75 | } 76 | return max(f[pow][l], f[pow][r - pow2 + 1]); 77 | } 78 | ``` 79 | 80 | 此处每次查询都计算了一遍 $2$ 的幂次,实际可以先预处理计算出来,不需要每次都算一遍。 -------------------------------------------------------------------------------- /130~139/137【数据结构】Kruskal 重构树.md: -------------------------------------------------------------------------------- 1 | **Kruskal 重构树**:维护图上两点间所有简单路径的最大边权的最小值 / 维护树上两点间路径的最大边权的数据结构。 2 | 3 | (也可以维护图上两点间所有简单路径的最小边权的最大值 / 维护树上两点间路径的最小边权) 4 | 5 | 6 | 7 | ## 建树 8 | 9 | 考虑 [Kruskal](https://io.zouht.com/32.html) 算法过程: 10 | 11 | 1. 将边按权重从小到大排序 12 | 2. 重复进行下面操作: 13 | - 取出最小的一条边 $u\overset{w}\leftrightarrow v$ 14 | - 如果加入这条边后不会成环:加入这条边。 15 | - 如果加入这条边后成环:跳过这条边,重复上一步操作。 16 | 3. 若所有边全部考察完毕或已经生成出一棵树,则停止算法。 17 | 18 | Kruskal 重构树是在上面这个过程同时建立的。重构树可以用一个链式前向星储存,此外还需要一个 $\text{val}_i$ 数组储存重构树的点权: 19 | 20 | 若选择了 $u\overset{w}\leftrightarrow v$ 这条边,则在重构树中添加一个新节点 $k$,节点权值 $\text{val}_k=w$,然后需要新增两条边:$k\to\text{find}(u)$ 和 $k\to\text{find}(v)$. 其中 $\text{find}(\cdot)$ 就是 Kruskal 算法里并查集的函数,另外,在生成重构树之外,Kruskal 算法的其他操作照做。 21 | 22 | 建树代码片段如下: 23 | 24 | ```cpp 25 | vector> edge(n + 10); // 0-u, 1-v, 2-w 26 | for (int i = 1; i < n; i++) 27 | cin >> edge[i][0] >> edge[i][1] >> edge[i][2]; 28 | sort(edge.begin() + 1, edge.begin() + n, [](array a, array b) { return a[2] < b[2]; }); 29 | int pos = n + 1; 30 | for (int i = 1; i < n; i++) 31 | { 32 | auto &[u, v, w] = edge[i]; 33 | int fa_u = find(u), fa_v = find(v); 34 | if (fa_u == fa_v) 35 | continue; 36 | krus[pos].push_back(fa_u); 37 | krus[pos].push_back(fa_v); 38 | val[pos] = w; 39 | fa[fa_u] = pos; 40 | fa[fa_v] = pos; 41 | pos++; 42 | } 43 | ``` 44 | 45 | 一个 Kruskal 重构树的示例,左图为原图,其中加粗边为生成树的边,右图为对应的 Kruskal 重构树,节点旁的数字为点权。 46 | 47 | | | | 48 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 49 | 50 | ## 性质 51 | 52 | 重构树中所有叶子节点为原图中的节点,重构树中所有内部节点为最小生成树中选择的边。 53 | 54 | 下面三个值大小相同: 55 | 56 | 1. 原图中两个点之间的**所有**简单路径上**最大**边权的最小值 57 | 2. 最小生成树上两个点之间的简单路径上的**最大**边权 58 | 3. Kruskal 重构树上两点的最近公共祖先的点权 59 | 60 | 首先考虑 1, 2 两条,这两条其实是最小生成树的性质。两个点之间的所有简单路径上最大边权的最小值一定会被最小生成树取到,如果不取的话就不能保证最小了。 61 | 62 | 然后考虑 2,3 两条,Kruskal 建立最小生成树的每一步,相当于将两个集合合并,且连接这两个集合的的大小一定 $\geq$ 两个集合里面的边,这个是 Kruskal 从小到大选边保证的。那么两个节点如果一个在左集合,一个在右集合,它们的最短路径肯定要跨越这个连接两个集合的边,在 Kruskal 重构树内,这个就是两个节点的 LCA,权值就是 LCA 的点权。 63 | 64 | ## 注 65 | 66 | 上述建树时和 Kruskal 计算最小生成树一致,维护的是图上两点间所有简单路径的**最大边权**的最小值。 67 | 68 | 如果将边从大到小排序,即相当于计算最大生成树,此时维护的是图上两点间所有简单路径的**最小边权**的最大值。 -------------------------------------------------------------------------------- /100~109/100【算法】矩阵加速算法.md: -------------------------------------------------------------------------------- 1 | **矩阵加速算法**:使用矩阵加速数列递推式的计算。 2 | 3 | 4 | 5 | ## 构造矩阵形式递推式 6 | 7 | 例如递推式: 8 | $$ 9 | a_n=a_{n-1}+a_{n-3},\ 其中\ a_1=a_2=a_3=1 10 | $$ 11 | 如果直接用这个多项式形式递推式来进行递推,时间复杂度为:$O(n)$,接下来的操作就是为了加速这个过程。 12 | 13 | 我们列出递推式对应的方程组: 14 | $$ 15 | \begin{cases} 16 | a_n&=&a_{n-1}&&+a_{n-3}\\ 17 | a_{n-1}&=&a_{n-1}&\\ 18 | a_{n-2}&=&&+a_{n-2} 19 | \end{cases} 20 | $$ 21 | 写成矩阵形式: 22 | $$ 23 | \begin{bmatrix} 24 | a_{n}\\ 25 | a_{n-1}\\ 26 | a_{n-2} 27 | \end{bmatrix} 28 | = 29 | \begin{bmatrix} 30 | 1 & 0 & 1\\ 31 | 1 & 0 & 0\\ 32 | 0 & 1 & 0 33 | \end{bmatrix} 34 | \begin{bmatrix} 35 | a_{n-1}\\ 36 | a_{n-2}\\ 37 | a_{n-3} 38 | \end{bmatrix} 39 | $$ 40 | 那么即可得到每一项: 41 | $$ 42 | \begin{bmatrix} 43 | a_{n}\\ 44 | a_{n-1}\\ 45 | a_{n-2} 46 | \end{bmatrix} 47 | = 48 | \begin{bmatrix} 49 | 1 & 0 & 1\\ 50 | 1 & 0 & 0\\ 51 | 0 & 1 & 0 52 | \end{bmatrix}^{n-3} 53 | \begin{bmatrix} 54 | 1\\ 55 | 1\\ 56 | 1 57 | \end{bmatrix} 58 | $$ 59 | 60 | ## 矩阵快速幂 61 | 62 | 由于矩阵乘法也满足 $M^{2n}=(M^2)^n$,因此类比数乘快速幂,矩阵也可以用快速幂算法,只需将数乘换成矩阵乘法即可。 63 | 64 | 对于上述例子: 65 | $$ 66 | \begin{bmatrix} 67 | a_{n+3}\\ 68 | a_{n+2}\\ 69 | a_{n+1} 70 | \end{bmatrix} 71 | = 72 | M^{n} 73 | \begin{bmatrix} 74 | 1\\ 75 | 1\\ 76 | 1 77 | \end{bmatrix} 78 | ,\ 其中\ M=\begin{bmatrix} 79 | 1 & 0 & 1\\ 80 | 1 & 0 & 0\\ 81 | 0 & 1 & 0 82 | \end{bmatrix} 83 | $$ 84 | 通过快速幂来处理 $M^n$,时间复杂度为 $O(\log n)$,实现了递推计算的加速。 85 | 86 | ## 模板 87 | 88 | $n$ 阶方阵乘法: 89 | 90 | ```cpp 91 | vector> mat_mul(vector> a, vector> b, ll mod) 92 | { 93 | int n = a.size(); 94 | vector> res(n, vector(n)); 95 | for (int i = 0; i < n; i++) 96 | for (int j = 0; j < n; j++) 97 | for (int k = 0; k < n; k++) 98 | res[i][j] = (res[i][j] + a[i][k] * b[k][j] % mod) % mod; 99 | return res; 100 | } 101 | ``` 102 | 103 | 矩阵快速幂: 104 | 105 | ```cpp 106 | vector> mat_pow(vector> a, ll b, ll mod) 107 | { 108 | int n = a.size(); 109 | vector> res(n, vector(n)); 110 | for (int i = 0; i < n; i++) 111 | res[i][i] = 1; 112 | while (b) 113 | { 114 | if (b % 2) 115 | res = mat_mul(res, a, mod); 116 | a = mat_mul(a, a, mod); 117 | b /= 2; 118 | } 119 | return res; 120 | } 121 | ``` 122 | 123 | -------------------------------------------------------------------------------- /180~189/187【机器学习】简单偏好优化 (SimPO).md: -------------------------------------------------------------------------------- 1 | **简单偏好优化** (Simple Preference optimization, SimPO):大语言模型强化学习中的一种偏好优化方法,其对齐了偏好优化目标中的奖励函数生成指标,同时解放了参考模型,相比 DPO 更简单、稳定与高效。 2 | 3 | 4 | 5 | 论文:https://arxiv.org/abs/2405.14734 6 | 7 | # 1 DPO 的问题 8 | 9 | 回顾 DPO,它的目标函数是: 10 | $$ 11 | \mathcal{L}_{\text{DPO}}(\pi_\theta;\pi_\mathrm{ref})=-\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\left[\log\sigma(\beta\log\frac{\pi_{\theta}(y_w\mid x)}{\pi_{\mathrm{ref}}(y_w\mid x)}-\beta\log\frac{\pi_{\theta}(y_l\mid x)}{\pi_{\mathrm{ref}}(y_l\mid x)})\right] 12 | $$ 13 | 它的奖励函数是: 14 | $$ 15 | r(x,y)=\beta\log\frac{\pi_{r}(y\mid x)}{\pi_{\mathrm{ref}}(y\mid x)}+\beta\log Z(x)\\ 16 | \text{其中,}\;Z(x)=\sum_{y}\pi_{\mathrm{ref}}(y\mid x)\exp\left({\frac{1}{\beta}r(x,y)}\right) 17 | $$ 18 | 可以发现 DPO 有以下的问题: 19 | 20 | 1. DPO 在训练时需要参考模型 $\pi_{\mathrm{ref}}$,这带来了额外的内存和计算开销。 21 | 2. 由于推理时没有 $\pi_{\mathrm{ref}}$,DPO 偏好优化时的奖励函数和推理时的生成指标不一致。 22 | 23 | 第一点是显而易见的,下面展开分析一下第二点。 24 | 25 | 在生成阶段,策略模型 $\pi_{\theta}$(也就是 LLM)会生成一个最大的平均对数似然序列: 26 | $$ 27 | p_{\theta}(y\mid x)=\frac{1}{|y|}\log\pi_{\theta}(y\mid x)=\frac{1}{|y|}\sum_{i=1}^{|y|}\log\pi_{\theta}(y_i\mid x,y_{r(x,y_l)$ 这个条件并不一定满足生成时 $p_{\theta}(y_w\mid x)>p_{\theta}(y_l\mid x)$. 根据研究者的实验,使用 DPO 训练时只有约 $50\%$ 的三元组满足这个条件,这个情况是很糟糕的。 33 | 34 | SimPO 便是针对 DPO 的这两个问题进行优化,其对齐了偏好优化目标中的奖励函数与生成指标,同时无需参考模型。 35 | 36 | # 2 SimPO 方法 37 | 38 | ## 2.1 长度归一化奖励公式 39 | 40 | 为了对齐训练和推理,同时消去参考模型,SimPO 的想法很简单,直接拿生成时的指标来做训练时的奖励函数不就行了: 41 | $$ 42 | r_{\mathrm{SimPO}}(x,y)=\frac{\beta}{|y|}\log\pi_{\theta}(y\mid x)=\frac{\beta}{|y|}\sum_{i=1}^{|y|}\log\pi_{\theta}(y_i\mid x,y_{0$. 53 | 54 | 这个变动造成的影响比较好理解,就是获胜的奖励 $r(x,y_w)$ 必须比失败的奖励 $r(x,y_l)$ 大至少 $\gamma$. 55 | 56 | ## 2.3 目标函数 57 | 58 | 综上,结合 2.1, 2.2,SimPO 最终的目标函数便是: 59 | $$ 60 | \mathcal{L}_{\text{DPO}}(\pi_\theta)=-\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\left[\log\sigma\left(\frac{\beta}{|y_w|}\log\pi_{\theta}(y_w\mid x)-\frac{\beta}{|y_l|}\log\pi_{\theta}(y_l\mid x)-\gamma\right)\right] 61 | $$ 62 | -------------------------------------------------------------------------------- /001~009/009【题目】木棍游戏.md: -------------------------------------------------------------------------------- 1 | **牛客小白月赛43** 2 | 3 | C - 木棍游戏 4 | 5 | https://ac.nowcoder.com/acm/contest/11220/C 6 | 7 | 8 | 9 | 时间限制:C/C++ 1秒,其他语言2秒 10 | 空间限制:C/C++ 262144K,其他语言524288K 11 | Special Judge, 64bit IO Format: %lld 12 | 13 | ## 题目描述 14 | 15 | 给出 $n$ 根长度不一的木棍,第 $i$ 根棍子长度为 $a_i$ 。两根长度分别为 $a_b$ 和 $a_c$ 的木棍可以拼接成一根长度为 $a_b+a_c$ 的木棍,同理 $3$ 根, $4$ 根,甚至 $n$ 根都能拼接。 16 | 17 | 问:使用这 $n$ 根木棍作三角形的边(一根木棍**至多**使用一次,也可以不使用),能拼出的面积最大的三角形的面积。 18 | 19 | ## 输入描述: 20 | 21 | > 第一行包含一个整数 $n$ $(3 \le n \le 8)$,表示木棍的数量。 22 | > 第二行包含 $n$ 个整数,用空格隔开,表示 $n$ 根木棍的分别长度 $a_1,a_2,...,a_n$ 其中 $1\le a_i\le 1e3$。 23 | 24 | ## 输出描述: 25 | 26 | > 输出一行,表示能拼出来的最大三角形的面积,结果保留一位小数。如果无法拼出三角形,输出$-1$。 27 | 28 | ## 示例1 29 | 30 | ### 输入 31 | 32 | > 3 33 | > 3 4 5 34 | 35 | ### 输出 36 | 37 | > 6.0 38 | 39 | ## 示例2 40 | 41 | ### 输入 42 | 43 | > 3 44 | > 3 4 7 45 | 46 | ### 输出 47 | 48 | > -1 49 | 50 | 51 | 52 | ## 我的笔记 53 | 54 | ### 分析 55 | 56 | 木棍数量最大为 8,每个木棍有 4 个状态:不用、边 a、边 b、边 c,如果将每个状态全部枚举,时间复杂度为 $O(4^n)$,最大 $4^8=65536$,时间复杂度在合理范围内,因此直接搜索一遍所有情况。 57 | 58 | 使用 DFS,枚举每种情况,到达目标时,计算出对应的边 a、边 b、边 c,使用海伦公式,若根号下是正数,即这个三角形成立,若是负数,则无法构成三角形。 59 | 60 | 答案初值设置为 0,每次求出答案保留最大值,最后若答案为 0,则无法构成,若答案非 0,则输出答案。 61 | 62 | ### 源码 63 | 64 | ```cpp 65 | #include 66 | 67 | using namespace std; 68 | 69 | int n, length[8], choice[8]; 70 | bool flag[8]; 71 | double ans = 0; 72 | 73 | void dfs(int x) 74 | { 75 | if (x == n) 76 | { 77 | int a = 0, b = 0, c = 0; 78 | for (int i = 0; i < n; i++) 79 | { 80 | if (choice[i] == 0) 81 | { 82 | continue; 83 | } 84 | else if (choice[i] == 1) 85 | { 86 | a += length[i]; 87 | } 88 | else if (choice[i] == 2) 89 | { 90 | b += length[i]; 91 | } 92 | else if (choice[i] == 3) 93 | { 94 | c += length[i]; 95 | } 96 | } 97 | double p = 1.0 * (a + b + c) / 2; 98 | double spow2 = p * (p - a) * (p - b) * (p - c); 99 | if (spow2 > 0) 100 | ans = max(ans, sqrt(spow2)); 101 | return; 102 | } 103 | for (int i = 0; i < 4; i++) 104 | { 105 | if (!flag[x]) 106 | { 107 | flag[x] = true; 108 | choice[x] = i; 109 | dfs(x + 1); 110 | flag[x] = false; 111 | } 112 | } 113 | } 114 | 115 | int main(void) 116 | { 117 | cin >> n; 118 | for (int i = 0; i < n; i++) 119 | { 120 | cin >> length[i]; 121 | } 122 | dfs(0); 123 | if (ans != 0) 124 | printf("%.1lf\n", ans); 125 | else 126 | printf("-1\n"); 127 | return 0; 128 | } 129 | ``` 130 | 131 | -------------------------------------------------------------------------------- /170~179/177【机器学习】残差神经网络.md: -------------------------------------------------------------------------------- 1 | **残差神经网络 **(Residual Neural Network, ResNet): 属于深度学习模型的一种,其核心在于让网络的每一层不直接学习预期输出,而是学习与输入之间的残差关系。 2 | 3 | 4 | 5 | # 1 背景 6 | 7 | 由于梯度爆炸/梯度消失的问题,越深的深度学习模型越难收敛。通过归一化可以解决这个问题,让几十层的深度学习模型能够收敛。但是即使几十层的深度学习模型能够收敛,它的准确率反而不如浅层的模型,这个问题被称为“性能退化”问题。 8 | 9 | 10 | 11 | > 图片来源:https://arxiv.org/abs/1512.03385 12 | 13 | 理论上,通过添加网络层给一个模型加深不应该导致更高的训练误差,如果这些额外层被设置为特征映射,深层的模型至少能和浅层部分表现相同。但实际情况表明了,优化器并不能有效地将这些层的参数设定为特征映射。 14 | 15 | > **特征映射** (Identity Mapping): 是指一个网络层或者模型结构中的一种操作,其作用是将输入直接传递到输出,不进行任何变换或者操作。换句话说,特征映射保持输入的信息不变,只是简单地将输入复制到输出。 16 | 17 | # 2 残差网络 18 | 19 | 如果原始层希望拟合的映射为 $\mathcal{H}(\mathrm{x})$,在残差网络中,我们就改为拟合残差,即层输入和输出的差值: 20 | $$ 21 | \mathcal{F}(\mathrm{x})=\mathcal{H}(\mathrm{x})-\mathrm{x} 22 | $$ 23 | 那么原始的映射就可以写作: 24 | $$ 25 | \mathcal{H}(\mathrm{x})=\mathcal{F}(\mathrm{x})+\mathrm{x} 26 | $$ 27 | 上述式子可以理解为,在原始的网络上进行一个跳接,将层的输出和跳接的输入相加后再进行激活,如下图: 28 | 29 | ![](https://assets.zouht.com/img/note/177-02.webp) 30 | 31 | > 图片来源:https://arxiv.org/abs/1512.03385 32 | 33 | 设计以上这些结构,一个重要的原因是让优化器把残差层设置为特征映射比普通层更简单。因为只需将残差层置为 $0$​ 即代表了特征映射,而普通层还需要复杂调整来实现特征映射。因此,残差网络更有可能保证模型加深时的准确度。 34 | 35 | # 3 前向传播 36 | 37 | 前向传播非常简单,就是上一节所说,添加一个跳接,将层的输出和跳接输入相加后再激活。但是有一个需要注意的点是,跳接并不是一定只能跳接两层,是可以跳接任意层数的(但注意,跳接一层是无意义的). 38 | 39 | 综上,一个残差块可以记作: 40 | $$ 41 | \mathrm{y}=\mathcal{F}(\mathrm{x})+\mathrm{x} 42 | $$ 43 | 其中,$\mathrm{x}$ 和 $\mathrm{y}$ 就是残差块的输入和输出,$\mathcal{F}$ 代表了残差映射,它可以是多个层堆叠。例如跳接两层的话: 44 | $$ 45 | \mathcal{F}=W_2\sigma(W_1\mathrm{x}) 46 | $$ 47 | 48 | > 需要注意的是,$\mathcal{F}$ 最外侧是没有激活函数的,这次激活被移到了与跳接值相加后再进行。 49 | 50 | 接下来,给残差块的定义公式加上层数角标,转换一下写法: 51 | $$ 52 | \mathrm{x}_{k+1}=\mathcal{F}(\mathrm{x}_k)+\mathrm{x}_k 53 | $$ 54 | 然后递归应用此公式,就可以推导出: 55 | $$ 56 | \mathrm{x}_{K}=\mathrm{x}_{k}+\sum_{i=k}^{K-1}\mathcal{F}(\mathrm{x}_i) 57 | $$ 58 | 由上式可以看出,无论 $k,K$ 两层间隔多少,都会有一个信号能直接从 $k$ 传到 $K$ 层。 59 | 60 | # 4 反向传播 61 | 62 | 将误差 $E$ 对 $\mathrm{x}_k$ 进行求导: 63 | $$ 64 | \begin{align} 65 | \frac{\partial E}{\partial \mathrm{x_k}}&=\frac{\partial E}{\partial \mathrm{x}_K}\frac{\partial \mathrm{x}_K}{\partial \mathrm{x}_k}\\ 66 | &=\frac{\partial E}{\partial \mathrm{x}_K}(1+\frac{\partial}{\partial \mathrm{x}_k}\sum_{i=k}^{K-1}\mathcal{F}(\mathrm{x}_i))\\ 67 | &=\frac{\partial E}{\partial \mathrm{x}_K}+\frac{\partial E}{\partial \mathrm{x}_K}\frac{\partial}{\partial \mathrm{x}_k}\sum_{i=k}^{K-1}\mathcal{F}(\mathrm{x}_i) 68 | \end{align} 69 | $$ 70 | 通过上式也可以看出,残差网络能够解决梯度消失问题。浅层梯度 $\frac{\partial E}{\partial \mathrm{x_k}}$ 总会包含 $\frac{\partial E}{\partial \mathrm{x}_K}$,因此即使 $\mathcal{F}$ 的梯度很小,也能保证总体梯度不消失。 71 | -------------------------------------------------------------------------------- /160~169/160【题目】维克多词典.md: -------------------------------------------------------------------------------- 1 | **第九届中国大学生程序设计竞赛(秦皇岛)-(CCPC2023-Qinhuangdao)** 2 | 3 | J - 维克多词典 4 | 5 | https://cpc.csgrandeur.cn/csgoj/problemset/problem?pid=1233 6 | 7 | 8 | 9 | 1 second / 256 megabytes 10 | 11 | standard input / standard output 12 | 13 | ## 题目 14 | 15 | Keyi 是一个热爱读书的中学生,并养成了每天早上阅读的习惯。 16 | 17 | 随着高考的临近,Keyi 计划每天从《维克多词典》中学习几个单词。 18 | 19 | 然而,她记忆单词的方法有点奇特。如果她今天学习了一个长度为 $k$ 的单词,她坚持要在当天记住词典中**所有**长度为 $k$ 的单词。 20 | 21 | 但是,Keyi 每天的精力是有限的,她一天最多只能学习 $W$ 个单词,否则她就记不住它们了。 22 | 23 | Keyi 不确定要如何有效地安排学习计划,以便尽量用最少的天数完成《维克多词典》的记忆。因此,她恳请您的帮助。 24 | 25 | ## 输入 26 | 27 | 第一行包含两个整数 $n$ 和 $W\ (1\le W \le n \le 50000)$,表示《维克多词典》内单词的数量以及 Keyi 每天最多学习的单词数量。 28 | 29 | 第二行包含 $n$ 个整数 $a_i\ (1 \le a_i \le 13)$,$a_i$ 表示第 $i\ (1 \le i \le n)$ 个单词的长度。 30 | 31 | 保证对于相同长度的单词,它们不会出现超过 $W$ 次。 32 | 33 | ## 输出 34 | 35 | 输出一行表示 Keyi 记住整本《维克多词典》所需的最少天数。 36 | 37 | ## 样例 38 | 39 | **输入** 40 | 41 | > 5 4 42 | > 1 2 1 2 1 43 | 44 | **输出** 45 | 46 | > 2 47 | 48 | ## 题解 49 | 50 | 词长度范围仅 $13$,很小,可以考虑状态压缩 DP. 使用 $13$ 位的二进制数表示状态,第 $i$ 位为 $1$ 代表该词已经被学习,反之没有,遍历 $2^{13}$ 个状态进行转移即可。 51 | 52 | 对于一个状态 $i$,它可以通过学习它的非空子集 $j$ 转移得到,学习前的状态为 $i\oplus j$. 53 | 54 | 例如对于 $i=0000000001011_2$,它可以通过七种学习方式 $0000000001011_2$,$0000000000011_2$,$0000000001001_2$,$0000000001010_2$,$0000000000001_2$,$0000000000010_2$,$0000000001000_2$ 转移得到。 55 | 56 | 遍历一个状态的非空子集的方式可以写作:`for (int j = i; j; j = (j - 1) & i)` 57 | 58 | 但是,如果一次学习的量超过 $w$,那么也是不成立的,因此每次需要判断学习的量有没有超标。可以预处理出来每种学习方式的数量。 59 | 60 | ## 代码 61 | 62 | ```cpp 63 | #include 64 | #define endl '\n' 65 | #define int long long 66 | 67 | using namespace std; 68 | 69 | constexpr int MAXN = 13; 70 | int cnt[MAXN]; 71 | int n, w; 72 | int sum[1 << MAXN], dp[1 << MAXN]; 73 | 74 | void solve() 75 | { 76 | memset(dp, 0x3f, sizeof(dp)); 77 | dp[0] = 0; 78 | cin >> n >> w; 79 | for (int i = 0; i < n; i++) 80 | { 81 | int t; 82 | cin >> t; 83 | cnt[t - 1]++; 84 | } 85 | for (int i = 0; i < (1 << MAXN); i++) 86 | for (int j = 0; j < MAXN; j++) 87 | if (i >> j & 1) 88 | sum[i] += cnt[j]; 89 | for (int i = 0; i < (1 << MAXN); i++) 90 | for (int j = i; j; j = (j - 1) & i) 91 | if (sum[j] <= w) 92 | dp[i] = min(dp[i], dp[i ^ j] + 1); 93 | cout << dp[(1 << MAXN) - 1] << endl; 94 | } 95 | 96 | signed main() 97 | { 98 | ios::sync_with_stdio(false); 99 | cin.tie(0); 100 | cout.tie(0); 101 | int t = 1; 102 | // cin >> t; 103 | while (t--) 104 | solve(); 105 | return 0; 106 | } 107 | ``` 108 | 109 | -------------------------------------------------------------------------------- /180~189/188【机器学习】组相关策略优化 (GRPO).md: -------------------------------------------------------------------------------- 1 | **组相关策略优化** (Group Relative Policy Optimization, GRPO):强化学习中的一种策略优化方法,通过采样求期望节省了 PPO 中的 Value (Critic) 模型。Deepseek-R1 的训练方法。 2 | 3 | 4 | 5 | - [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://arxiv.org/abs/2402.03300) 6 | - [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://arxiv.org/abs/2501.12948) 7 | 8 | # 1 回顾 [PPO](https://io.zouht.com/185.html) 9 | 10 | 在 PPO 中,我们需要四个模型: 11 | 12 | 1. Policy (Actor) Model 13 | 2. Value (Critic) Model 14 | 3. Reference Model 15 | 4. Reward Model 16 | 17 | 其中,参数更新的是 Policy 和 Value 模型,Reference 和 Reward 的参数是不训练的。另外,通常这 4 个模型都是相同参数规模的,也就是说我们需要目标模型四倍大小的显存来进行训练,显存和算力消耗很大。 18 | 19 | 为了降低开销,涌现出了不少策略优化算法,针对的就是怎么省去这四个模型中的其中一个模型。例如之前的笔记中,DPO 省掉了 Value 模型同时是 Off-Policy 算法,SimPO 省掉了 Value 和 Reference 模型。 20 | 21 | 而本篇笔记分享的 GRPO 方法,就是通过采样求期望节省了 PPO 中的 Value 模型,只需要加载三个模型。 22 | 23 | # 2 GRPO 的差异点 24 | 25 | 要省掉 PPO 中的 Value 模型,首先得看看这个模型做了什么,有什么方法去近似它所做的工作。Value 模型的作用是评估当前 Policy 在特定 State 情况下,可以得到的 Reward 的**期望值**。最终基于 Value 模型给出的值,让奖励值更稳定,方差减小,从而让 RL 训练更稳定。 26 | 27 | 看到这个期望值,我们容易想到,期望可以用采样求均值的方法来近似。采样 Policy 这个过程不需要引入额外的模型,因此通过采样求均值是可以省掉 Value 模型的。这便是 GRPO 朴素简单的思想。 28 | 29 | ![](https://assets.zouht.com/img/note/188-01.webp) 30 | 31 | 接下来,根据上图对比一下 PPO 和 GRPO 的差异。 32 | 33 | 首先最明显的就是上文说过的省去了 Value 模型,可以看到 GRPO 一次采样了 $G$ 个输出 $o_1,o_2,\dots,o_G$. 对于这 $G$ 个输出,通过 Reward 模型获得每个输出的奖励值 $r_1,r_2,\dots,r_G$. 然后通过计算这 $G$ 个采样的均值和方差,获得最终的优势值 $A_1,A_2,\dots,A_G$. 然后基于优势值来指导模型更新。 34 | 35 | 除此之外,还能注意到,$\text{KL}$ 惩罚的施加位置不同。PPO 的 $\text{KL}$ 惩罚是逐 Token 计算的,而 GRPO 的 $\text{KL}$ 惩罚是直接拿 Policy 和 Reference 算出来施加到最终奖励值的。 36 | 37 | # 3 GRPO 的细节 38 | 39 | GRPO 的目标函数是: 40 | $$ 41 | \begin{align} 42 | &\max_{\theta}\ \mathbb{E}[(s_t,a_t)\sim\pi_{\theta'},\{o_i\}_{i=1}^{G}\sim\pi'(a_t|s_t)]\\ 43 | &\frac{1}{G}\sum_{i=1}^{G}\left[\min\left(\frac{p_{\theta}(a_t\mid s_t)}{p_{\theta'}(a_t\mid s_t)}A_i,\mathrm{clip}\left(\frac{p_{\theta}(a_t\mid s_t)}{p_{\theta'}(a_t\mid s_t)},1-\epsilon,1+\epsilon\right)A_i\right)-\beta\mathbb{D}_{KL}(\pi_\theta\Vert\pi_{\mathrm{ref}})\right] 44 | \end{align} 45 | $$ 46 | 其中: 47 | $$ 48 | \begin{align} 49 | \mathbb{D}_{KL}(\pi_\theta\Vert\pi_{\mathrm{ref}})&=\frac{\pi_{\mathrm{ref}}(a_t\mid s_t)}{\pi_{\theta}(a_t\mid s_t)}-\log \frac{\pi_{\mathrm{ref}}(a_t\mid s_t)}{\pi_{\theta}(a_t\mid s_t)}-1\\ 50 | A_i&=\overset{\sim}{r_i}=\frac{r_i-\mathrm{mean}(\{r_1,r_2,\cdots,r_G\})}{\mathrm{std}(\{r_1,r_2,\cdots,r_G\})} 51 | \end{align} 52 | $$ 53 | 可以看到,GRPO 的目标函数和 PPO 的结构几乎一致,重要区别就是 $A_i$ 的计算方式是将 $G$ 个采样的奖励值 $\{r_1,r_2,\cdots,r_G\}$ 归一化。 54 | 55 | 另外,还需要注意 GRPO 估计 $\text{KL}$ 惩罚时,使用了一种无偏的蒙特卡洛方法:http://joschu.net/blog/kl-approx.html 56 | -------------------------------------------------------------------------------- /060~069/064【算法】Bellman-Ford 算法.md: -------------------------------------------------------------------------------- 1 | **解决赋权图的单源最短路径问题:**Bellman-Ford (贝尔曼-福特) 算法 2 | 3 | - 能解决负边 4 | - 能解决负环 5 | 6 | 7 | 8 | # Bellman-Ford 算法 9 | 10 | ## 复杂度 11 | 12 | 时间复杂度:$O(\left|V\right|\cdot\left|E\right|)$ 13 | 14 | ($\left|V\right|$ 为顶点数,$\left|E\right|$ 为边数) 15 | 16 | ## 分析 17 | 18 | Dijkstra 算法仅对确定点的出边做松弛操作,Bellman-Ford 算法更为暴力,每次迭代对所有的边做松弛操作,共进行 $\left|V\right|-1$ 次迭代。在重复的计算中,最短路径可以被多次更新,正确最短距离的边随着计算不断增加,直到最后一次迭代,所有的边都计算正确。这也是为什么该算法能够处理赋权边。 19 | 20 | 伪代码如下: 21 | 22 | ``` 23 | bellman_ford(G, dist[]) 24 | 初始化: dist[1~V]=+INF, dist[1]=0 25 | for (循环V-1次) 26 | for (t : 所有边的集合) 27 | if (以t为中介点到s的距离更短) 28 | 更新dist[s]; 29 | ``` 30 | 31 | #### 迭代次数 32 | 33 | 上述进行 $\left|V\right|-1$ 次迭代实际上是最大迭代次数,实际有可能在 $\left|V\right|-1$ 次前就已经计算出了所有最短路径。因此可以增加判断,若一次迭代中没有发生松弛操作,即可跳出循环结束算法。 34 | 35 | 迭代次数实际上也有意义,若进行 $k$ 次迭代,产生的结果便是经过 $\leq k$ 个边的最短路径。因此该算法可以很好地解决“有边数限制的最短路问题”。 36 | 37 | #### 负权回路 38 | 39 | 当图中出现负权回路时,有可能不存在最短路,示例如下: 40 | 41 | 42 | 43 | $B\rightarrow D\rightarrow C\rightarrow\cdots$ 为一个回路,该回路的权值为 $1+(-2)+(-1)=-2$,即负权回路。若要求 $A\rightarrow E$ 的最短路,我们可以在负权回路中绕任意次,每绕一次路径长度 $-2$。因此 $A\rightarrow E$ 的距离可以为 $-\infty$,因此不存在最短路径。 44 | 45 | 若要判断是否有负权回路,可以在 $\left|V\right|-1$ 次后额外进行一次迭代,若第 $\left|V\right|$ 次迭代又发生了松弛操作,则可以确定图中存在负权回路。 46 | 47 | #### 无穷大判断 48 | 49 | 由于在代码实现中我们往往使用一个较大的数作为 $\infty$,例如 0x3f3f3f3f. 而在该算法中,有可能出现以下松弛操作: 50 | 51 | 52 | 53 | 松弛后,后者的值变成了 $\infty-1$。虽然在数学中 $\infty-1=\infty$,但在程序中 $\text{0x3f3f3f3f}$ 不等于 $\text{0x3f3f3f3f}-1$,此时对于无穷大的判断就会产生问题。为了规避这个问题,我们可以判断一个数若 $\geq \text{0x3f3f3f3f}/2$ 就为 $\infty$,在一般情况下,是不会出现错误的。 54 | 55 | #### 串联问题 56 | 57 | 在代码实现中,若每次迭代使用的就是本次迭代的值,很有可能产生串联问题,即本次迭代中前面计算出的值被后面的计算使用,这样会产生错误。为了限制本次迭代使用的值一定为上次迭代的值,我们需要备份保存一遍上次的值,对应下面代码中 memcpy 语句。 58 | 59 | ## 代码实现 60 | 61 | 由于该算法只需遍历所有的边即可,所以存图方法比较简单粗暴,直接用一个结构体数组存下来所有边即可。 62 | 63 | ```cpp 64 | const int MAXN = 510, MAXM = 1e4 + 10, INF = 0x3f3f3f3f; 65 | int v, e, k; // v顶点数 e边数 k经过边数限制 66 | int dist[MAXN], backup[MAXN]; // dist最短距离 backup备份上一次迭代 67 | struct Edge 68 | { 69 | int a, b, w; // a起点 b终点 w权值 70 | } edges[MAXM]; // 结构图数组存边 71 | 72 | int bellman_ford() 73 | { 74 | memset(dist, 0x3f, sizeof(dist)); 75 | dist[1] = 0; 76 | for (int i = 0; i < k; i++) 77 | { 78 | memcpy(backup, dist, sizeof(dist)); 79 | for (int j = 0; j < e; j++) 80 | { 81 | int a = edges[j].a, b = edges[j].b, w = edges[j].w; 82 | dist[b] = min(dist[b], backup[a] + w); 83 | } 84 | } 85 | return dist[v] > INF / 2 ? INF : dist[v]; 86 | } 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /070~079/077【题目】青蛙的约会.md: -------------------------------------------------------------------------------- 1 | **POJ1061.** 青蛙的约会 2 | 3 | http://poj.org/problem?id=1061 4 | 5 | 6 | 7 | 1000MS / 10000K 8 | 9 | ### Description 10 | 11 | 两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。 12 | 13 | 我们把这两只青蛙分别叫做青蛙 $A$ 和青蛙 $B$ ,并且规定纬度线上东经 $0$ 度处为原点,由东往西为正方向,单位长度 $1$ 米,这样我们就得到了一条首尾相接的数轴。设青蛙 $A$ 的出发点坐标是 $x$,青蛙 $B$ 的出发点坐标是 $y$。青蛙 $A$ 一次能跳 $m$ 米,青蛙 $B$ 一次能跳 $n$ 米,两只青蛙跳一次所花费的时间相同。纬度线总长 $L$ 米。现在要你求出它们跳了几次以后才会碰面。 14 | 15 | ### Input 16 | 17 | 输入只包括一行 $5$ 个整数 $x$,$y$,$m$,$n$,$L$,其中 $x\neq y<2\cdot10^9$,$0 1 2 3 4 5 26 | 27 | ### Sample Output 28 | 29 | > 4 30 | 31 | ### 题解 32 | 33 | 扩展欧几里得算法基础题,扩欧算法笔记:https://io.zouht.com/67.html 34 | 35 | 在 $t$ 时刻时 ($t\in\mathbb{Z}$),$A$ 的位置为 $x+t\cdot m$,$B$ 的位置为 $y+t\cdot n$ 36 | 37 | 若此时 $A$ 与 $B$ 相遇,则满足: 38 | $$ 39 | (y+t\cdot n)+k\cdot L=(x+t\cdot m)\;(其中t,k\in\mathbb{Z}) 40 | $$ 41 | 变形可得到: 42 | $$ 43 | t\cdot(n-m)+k\cdot L=x-y\;(其中t,k\in\mathbb{Z}) 44 | $$ 45 | 不妨令 $a=n-m,b=L,c=x-y,d=\gcd(a,b)$,式子可化为: 46 | $$ 47 | a\cdot t+b\cdot k=c\tag{*} 48 | $$ 49 | 可发现这是一个关于未知数 $t$ 和 $k$ 的线性丢番图方程,当 $d\mid c$ 时有整数解,可通过扩欧算法得到: 50 | $$ 51 | a\cdot t'+b\cdot k'=d 52 | $$ 53 | 54 | 的解 $t',k'$,从而得到原方程 $(*)$ 的解: 55 | $$ 56 | \begin{cases} 57 | t_0=t'\cdot(c/d)\\ 58 | k_0=k'\cdot(c/d)\\ 59 | \end{cases} 60 | $$ 61 | 上面得到的为特解 $t_0$,我们要求的为最小正数。通过通解公式: 62 | $$ 63 | \begin{cases} 64 | t=t_0-p\cdot(b/d)\\ 65 | k=k_0+p\cdot(a/d)\\ 66 | \end{cases} 67 | $$ 68 | 69 | 我们可以找到最小正数解 $t$ 为: 70 | $$ 71 | t=[t_0\bmod (b/d)+(b/d)]\bmod(b/d) 72 | $$ 73 | 74 | ### 代码 75 | 76 | ```cpp 77 | #include 78 | 79 | using namespace std; 80 | 81 | typedef long long ll; 82 | 83 | ll exgcd(ll a, ll b, ll &x, ll &y) 84 | { 85 | if (!b) 86 | { 87 | x = 1; 88 | y = 0; 89 | return a; 90 | }; 91 | ll d = exgcd(b, a % b, y, x); 92 | y -= a / b * x; 93 | return d; 94 | } 95 | 96 | int main() 97 | { 98 | ll x, y, m, n, L; 99 | cin >> x >> y >> m >> n >> L; 100 | ll a = n - m, b = L, c = x - y; 101 | if (a < 0) 102 | { 103 | a = -a; 104 | c = -c; 105 | } 106 | ll p, q; 107 | ll d = exgcd(a, b, p, q); 108 | if (c % d) 109 | cout << "Impossible" << endl; 110 | else 111 | cout << (p * (c / d) % (b / d) + (b / d)) % (b / d) << endl; 112 | return 0; 113 | } 114 | ``` 115 | -------------------------------------------------------------------------------- /140~149/141【题目】Insert 1, Insert 2, Insert 3, ....md: -------------------------------------------------------------------------------- 1 | **2023牛客暑期多校训练营8** 2 | 3 | H - Insert 1, Insert 2, Insert 3, ... 4 | 5 | https://ac.nowcoder.com/acm/contest/57362/H 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 给定长度为 $n$ 的序列 $\{a\}_{i=1}^n$,问有多少个连续子区间 $[l,r]$ 满足这个子区间可以通过若干次依次按顺序插入 $1,2,3\cdots,k$ 的子序列构成。$1\leq n\leq 10^6$,$1\leq a_i\leq n$。 12 | 13 | ## 题解 14 | 15 | **思路** 16 | 17 | 一个合法的区间一定以 $1$ 开头,那么就可以考虑对于每个右端点 $r$,计算出有多少个左侧的 $1$ 能和它组成合法区间,统计总数便是题目答案。 18 | 19 | 对于一个数 $x$,它必须与左侧的最近的一个还没有被匹配的 $x-1$ 匹配,如果没有能匹配的则不合法。如: 20 | $$ 21 | \hat1,\bar1,\bar2,\hat2,\hat3,\bar3,3 22 | $$ 23 | 数字顶上标记相同的代表匹配为一组,可以看到最后一个 $3$ 需要在左边找到一个 $2$ 匹配,但是在左边的 $2$ 已经全部被其他 $3$ 匹配占用了,没有剩余的还没有被匹配的 $2$。因此这个 $3$ 不合法,它的答案是 $0$. 24 | 25 | 如果匹配上了,那答案的数量如何计算。对于每个匹配的一组数,找到它们起点(也就是 $1$)和起点前面一共有几个 $1$,数量便是合法区间的数量了。如: 26 | $$ 27 | 1,\bar1,\hat1,\hat2,\hat3,\bar2,\bar3 28 | $$ 29 | 对于第一个 $3$,它的起点是第三个 $1$,前面一共 $3$ 个 $1$,因此答案是 $3$。对于第二个 $3$,它的起点是第二个 $1$,因此答案是 $2$。 30 | 31 | 不过有一个要点是,如果有一个不合法的数将序列分开,那么左边的 $1$ 就不能再被匹配了,因为如果想要匹配左边的 $1$ 必须跨过那个不合法的数,那这个区间肯定就不合法了。如: 32 | $$ 33 | 1,1,1,1,1,99,1,2,3 34 | $$ 35 | 对于最后一个 $3$,它的起点前面的 $1$ 被 $99$ 这个不合法的数挡住了,所以总共只有 $1$ 个能够匹配。 36 | 37 | **维护方式** 38 | 39 | 使用 $10^6$ 个栈(用 `vector` 而不是 `stack`,要不然 MLE)维护每个数的位置,对于一个数 $x$,在第 $x-1$ 个栈中找栈顶,如果是空栈则这个数不成立,如果找到栈中有数,则将其弹出,并在第 $x$ 个栈中压入 $x$ 的下标。如此就可以快速找到左侧的最近的一个还没有被匹配的 $x-1$ 的位置。 40 | 41 | 对于 $1$ 的个数的维护,使用单调栈完成。考虑到当从左到右考虑时,能匹配的 $1$ 的位置从右往左移动,且是一个单调的。因此可以用单调栈弹掉不合法的 $1$,栈中剩下的数目便是答案数。对于碰见不合法的数,直接把栈弹空就行。 42 | 43 | ## 代码 44 | 45 | ```cpp 46 | #include 47 | #define endl '\n' 48 | #define int long long 49 | 50 | using namespace std; 51 | 52 | constexpr int MAXN = 1e6 + 10; 53 | vector p[MAXN], s; 54 | 55 | void solve() 56 | { 57 | int n; 58 | cin >> n; 59 | int ans = 0; 60 | for (int i = 1; i <= n; i++) 61 | { 62 | int x; 63 | cin >> x; 64 | if (x == 1) 65 | { 66 | p[1].push_back(i); 67 | s.push_back(i); 68 | ans += s.size(); 69 | } 70 | else if (p[x - 1].empty()) 71 | { 72 | s.clear(); 73 | } 74 | else 75 | { 76 | int y = p[x - 1].back(); 77 | p[x - 1].pop_back(); 78 | p[x].push_back(y); 79 | while (s.size() && s.back() > y) 80 | s.pop_back(); 81 | ans += s.size(); 82 | } 83 | } 84 | cout << ans << endl; 85 | } 86 | 87 | signed main() 88 | { 89 | ios::sync_with_stdio(false); 90 | cin.tie(0); 91 | cout.tie(0); 92 | int t = 1; 93 | // cin >> t; 94 | while (t--) 95 | solve(); 96 | return 0; 97 | } 98 | ``` 99 | 100 | -------------------------------------------------------------------------------- /120~129/127【题目】Nazrin the Greeeeeedy Mouse.md: -------------------------------------------------------------------------------- 1 | **2023牛客暑期多校训练营5** 2 | 3 | H - Nazrin the Greeeeeedy Mouse 4 | 5 | https://ac.nowcoder.com/acm/contest/57359/H 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 有 $n$ 个物品,第 $i$ 个物品的体积是 $a_i$,价值是 $b_i$,有 $m$ 个背包,第 $i$ 个背包的容积是 $s_i$ 且 $s_{i-1}\leq s_i$. 12 | 13 | 物品需要从 $1$ 到 $n$ 顺序取,即取了第 $i$ 个物品后,$n$ 时,只考虑最大的 $n$ 个背包即可。 24 | 25 | 因为对于 $n$ 个物品,最多用掉 $n$ 个背包(每个背包装 $1$ 个),又因为背包大小单调不减 $s_{i-1}\leq s_i$,那么如果 $m>n$,取最靠后的最大的 $n$ 个背包一定是最优的方案。 26 | 27 | #### 预处理每个区间的 0/1 背包问题 28 | 29 | 对于每个区间 $[i,j]$,用 0/1 背包模型算出背包大小为 $k$ 时能取到的最大的价值。实际上就是固定起点为 $i$,跑一遍 $i\sim n$ 的 0/1 背包问题。代码中的 $f_{i,j,k}:=$ 起点为 $i$ 终点为 $j$ 的区间,背包大小为 $k$ 时能取到的最大的价值。 30 | 31 | 预处理的时间复杂度为:$O(n^2\max\{s_i\})$ 32 | 33 | #### 计算最终结果 34 | 35 | $dp_{i,k}:=$ 第 $1\sim i$ 个背包取区间 $[1,k]$ 的物品,能取到的最大价值。 36 | 37 | 转移方式为:$dp_{i,k}=dp_{i-1,j}+f_{j,k,w_i}$ 38 | 39 | 计算结果的时间复杂度为:$O(n^3)$ 40 | 41 | > 如果没有发现可只考虑 $n$ 个最大的背包,时间复杂度是 $O(n^2m)$,肯定是过不去的。 42 | 43 | ## 代码 44 | 45 | ```cpp 46 | #include 47 | #define endl '\n' 48 | // #define int long long 49 | 50 | using namespace std; 51 | 52 | constexpr int MAXN = 210; 53 | int f[MAXN][MAXN][MAXN]; 54 | int a[MAXN], b[MAXN], s[MAXN]; 55 | int dp[MAXN][2]; 56 | 57 | void solve() 58 | { 59 | int n, m; 60 | cin >> n >> m; 61 | for (int i = 1; i <= n; i++) 62 | cin >> a[i] >> b[i]; 63 | for (int i = 1; i <= m; i++) 64 | cin >> s[max(1, min(0, n - m) + i)]; 65 | m = min(m, n); 66 | for (int i = 1; i <= n; i++) 67 | { 68 | for (int j = i; j <= n; j++) 69 | { 70 | for (int k = 0; k <= 200; k++) 71 | { 72 | f[i][j][k] = f[i][j - 1][k]; 73 | if (k - a[j] >= 0) 74 | f[i][j][k] = max(f[i][j][k], f[i][j - 1][k - a[j]] + b[j]); 75 | } 76 | } 77 | } 78 | int p = 0; 79 | for (int i = 1; i <= m; i++) 80 | { 81 | for (int j = 0; j <= 200; j++) 82 | dp[j][p] = 0; 83 | for (int j = 0; j <= n; j++) 84 | for (int k = j + 1; k <= n; k++) 85 | dp[k][p] = max(dp[k][p], dp[j][p ^ 1] + f[j + 1][k][s[i]]); 86 | p ^= 1; 87 | } 88 | cout << dp[n][p ^ 1] << endl; 89 | } 90 | 91 | signed main() 92 | { 93 | ios::sync_with_stdio(false); 94 | cin.tie(0); 95 | cout.tie(0); 96 | int t = 1; 97 | // cin >> t; 98 | while (t--) 99 | solve(); 100 | return 0; 101 | } 102 | ``` -------------------------------------------------------------------------------- /030~039/035【题目】1111gal password.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 242** 2 | 3 | C - 1111gal password 4 | 5 | https://atcoder.jp/contests/abc242/tasks/abc242_c 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $300$ points 12 | 13 | ### Problem Statement 14 | 15 | Given an integer $N$, find the number of integers $X$ that satisfy all of the following conditions, modulo $998244353$. 16 | 17 | - $X$ is an $N$-digit positive integer. 18 | - Let $X_1,X_2,\dots,X_N$ be the digits of $X$ from top to bottom. They satisfy all of the following: 19 | - $1 \le X_i \le 9$ for all integers $1 \le i \le N$; 20 | - $|X_i-X_{i+1}| \le 1$ for all integers $1 \le i \le N-1$. 21 | 22 | ### Constraints 23 | 24 | - $N$ is an integer. 25 | - $2 \le N \le 10^6$ 26 | 27 | ------ 28 | 29 | ### Input 30 | 31 | Input is given from Standard Input in the following format: 32 | 33 | > $N$ 34 | 35 | ### Output 36 | 37 | Print the answer as an integer. 38 | 39 | ------ 40 | 41 | ### Sample Input 1 42 | 43 | > 4 44 | 45 | ### Sample Output 1 46 | 47 | > 203 48 | 49 | Some of the $4$-digit integers satisfying the conditions are $1111,1234,7878,6545$. 50 | 51 | ------ 52 | 53 | ### Sample Input 2 54 | 55 | > 2 56 | 57 | ### Sample Output 2 58 | 59 | > 25 60 | 61 | ------ 62 | 63 | ### Sample Input 3 64 | 65 | > 1000000 66 | 67 | ### Sample Output 3 68 | 69 | > 248860093 70 | 71 | Be sure to find the count modulo $998244353$. 72 | 73 | ### 我的笔记 74 | 75 | 用到了动态规划的思想: 76 | $$ 77 | \begin{align} 78 | &f(x):=满足题目条件数字x的总数\\ 79 | &f(1)=1,f(2)=1,f(3)=1,\cdots,f(9)=1\\ 80 | &f(1?)=f(1)+f(2)=2,f(2?)=f(1)+f(2)+f(3)=3,\cdots,f(9?)=f(9)+f(8)=2\\ 81 | &f(1??)=f(1?)+f(2?)=5,f(2??)=f(1?)+f(2?)+f(3?)=8,\cdots,f(9??)=f(9?)+f(8?)=5\\ 82 | &\vdots 83 | \end{align} 84 | $$ 85 | 这就是状态转移方程,依据此写一个循环即可。 86 | 87 | ### 代码 88 | 89 | ```cpp 90 | #include 91 | #define MOD 998244353 92 | 93 | using namespace std; 94 | unsigned long long dp[1000010][10]{}; 95 | 96 | int main(void) 97 | { 98 | int N; 99 | cin >> N; 100 | for (int i = 1; i <= 9; i++) 101 | dp[1][i] = 1; 102 | for (int i = 2; i <= N; i++) 103 | { 104 | dp[i][1] = dp[i - 1][1] + dp[i - 1][2]; 105 | dp[i][1] %= MOD; 106 | for (int j = 2; j <= 8; j++) 107 | { 108 | dp[i][j] = dp[i - 1][j + 1] + dp[i - 1][j] + dp[i - 1][j - 1]; 109 | dp[i][j] %= MOD; 110 | } 111 | dp[i][9] = dp[i - 1][9] + dp[i - 1][8]; 112 | dp[i][9] %= MOD; 113 | } 114 | long long ans = 0; 115 | for (int i = 1; i <= 9; i++) 116 | { 117 | ans += dp[N][i]; 118 | ans %= MOD; 119 | } 120 | cout << ans << endl; 121 | return 0; 122 | } 123 | ``` 124 | 125 | -------------------------------------------------------------------------------- /150~159/155【题目】雪菜的式子.md: -------------------------------------------------------------------------------- 1 | **武汉大学2023年新生程序设计竞赛** 2 | 3 | H - 雪菜的式子 4 | 5 | https://ac.nowcoder.com/acm/contest/66651/H 6 | 7 | 8 | 9 | 1s / 512MB 10 | 11 | ## 题目 12 | 13 | 雪菜定义 $S(n)$ 为前 $n$ 个质数任意幂次方相乘和 $1$ 构成的集合,例如 $S(2)=\{1,2^a,3^b,2^a\times 3^b\}(a,b\in\mathbb{N}^*)$. 14 | $$ 15 | f(n)=\sum_{a_1\in S(n)}\sum_{a_2\in S(n)}\cdots\sum_{a_k\in S(n)}\mu\left(\prod_{i=1}^k a_i\right) 16 | $$ 17 | 其中 $\mu$ 为莫比乌斯函数。 18 | $$ 19 | \mu(n)=\begin{cases} 20 | 1,&若\;n=1\\ 21 | (-1)^k,&若\;n=p_1p_2\dots p_k,\;p_i\;为不同的质数\\ 22 | 0,&若\;n\;有大于\;1\;的平方数因数 23 | \end{cases} 24 | $$ 25 | 给定 $n,k$,求 $f(n)$,答案对 $998244353$ 取模。 26 | 27 | ## 输入 28 | 29 | 第一行两个数 $n,k$ $(1\leq n,k <998244353)$ 30 | 31 | ## 输出 32 | 33 | 一个整数表示答案。 34 | 35 | ## 样例 36 | 37 | 输入 38 | 39 | > 2 2 40 | 41 | 输出 42 | 43 | > 1 44 | 45 | ## 题解 46 | 47 | 本题需要将原问题转化一下,转化后的问题就很好求解了。 48 | 49 | 首先要对 $\mu(n)$ 做一下转换,该函数的描述可以变成: 50 | $$ 51 | \mu(n)=\begin{cases} 52 | (-1)^k,&n\;可拆成\;k\;个互不相同的质数相乘,\;1\;看作拆成\;0\;个\\ 53 | 0,&其他情况 54 | \end{cases} 55 | $$ 56 | 因此,拆分结果每种质数的幂次不能 $>1$,否则 $\mu(n)$ 直接变成 $0$,这一项就不考虑了。 57 | 58 | 接下来,就可以对不同的 $\mu(x)$ 进行分类讨论了: 59 | 60 | - 若 $\mu(x)=(-1)^0$,$x$ 包含 $0$ 种质数,选法 $C_{n}^{0}$,这 $0$ 个质数分给 $a_1\sim a_k$ 组合,有 $k^0$ 种安排方式 61 | - 若 $\mu(x)=(-1)^1$,$x$ 包含 $1$ 种质数,选法 $C_{n}^{1}$,这 $1$ 个质数分给 $a_1\sim a_k$ 组合,有 $k^1$ 种安排方式 62 | - 若 $\mu(x)=(-1)^2$,$x$ 包含 $2$ 种质数,选法 $C_{n}^{2}$,这 $2$ 个质数分给 $a_1\sim a_k$ 组合,有 $k^2$ 种安排方式 63 | - $\vdots$ 64 | - 若 $\mu(x)=(-1)^n$,$x$ 包含 $n$ 种质数,选法 $C_{n}^{n}$,这 $n$ 个质数分给 $a_1\sim a_k$ 组合,有 $k^n$ 种安排方式 65 | 66 | 那么,最终答案便是: 67 | $$ 68 | \sum_{i=0}^{n}C_n^i\cdot k^i\cdot(-1)^i 69 | $$ 70 | 根据二项式定理: 71 | $$ 72 | (a+b)^n=\sum_{i=0}^n C_n^i\cdot a^i\cdot b^{n-i} 73 | $$ 74 | 原式可以变形: 75 | $$ 76 | \begin{align} 77 | &\sum_{i=0}^{n}C_n^i\cdot k^i\cdot(-1)^i\\ 78 | =&(-1)^{2i-n}\sum_{i=0}^{n}C_n^i\cdot k^i\cdot(-1)^{n-i}\\ 79 | =&(-1)^n(k-1)^n 80 | \end{align} 81 | $$ 82 | 最后,使用快速幂即可解决。 83 | 84 | 需要注意的是,不要将 $-1$ 和 $k-1$ 并到一起再用快速幂,负数不能用快速幂。 85 | 86 | 另外取模时,需要注意正负性。 87 | 88 | ## 代码 89 | 90 | ```cpp 91 | #include 92 | #define endl '\n' 93 | #define int long long 94 | 95 | using namespace std; 96 | 97 | const int MOD = 998244353; 98 | 99 | int qpow(int a, int b) 100 | { 101 | int ans = 1; 102 | a %= MOD; 103 | while (b) 104 | { 105 | if (b % 2) 106 | ans = ans * a % MOD; 107 | a = a * a % MOD; 108 | b /= 2; 109 | } 110 | return ans; 111 | } 112 | 113 | void solve() 114 | { 115 | int n, k; 116 | cin >> n >> k; 117 | int ans = qpow(k - 1, n); 118 | if (n % 2) 119 | ans *= -1; 120 | ans = (ans % MOD + MOD) % MOD; 121 | cout << ans << endl; 122 | } 123 | 124 | signed main() 125 | { 126 | ios::sync_with_stdio(false); 127 | cin.tie(0); 128 | cout.tie(0); 129 | int t = 1; 130 | // cin >> t; 131 | while (t--) 132 | solve(); 133 | return 0; 134 | } 135 | ``` 136 | 137 | -------------------------------------------------------------------------------- /030~039/032【算法】Kruskal 算法.md: -------------------------------------------------------------------------------- 1 | **求最小生成树(适合稀疏图):**Kruskal (克鲁斯卡尔) 算法 2 | 3 | - 最小生成树是一副连通加权无向图中一棵权值最小的生成树。 4 | 5 | 6 | 7 | # Kruskal 算法 8 | 9 | ## 时间复杂度 10 | 11 | 时间复杂度:$O(\left|E\right|\log\left|V\right|)$ 12 | 13 | $\left|E\right|$ 为边数,$\left|V\right|$ 为顶点数,该版本适合稀疏图,即 $\left|E\right|\ll\left|V\right|^2$ 的图。 14 | 15 | ## 代码实现 16 | 17 | ```cpp 18 | const int MAXN = 1e5 + 10, INF = 0x3f3f3f3f; 19 | int v, e; // v顶点数 e边数 20 | int fa[MAXN]; // 并查集 21 | struct Edge // 边的结构体,重载了<供sort() 22 | { 23 | int start, end, dist; 24 | bool operator<(const Edge &x) const 25 | { 26 | return dist < x.dist; 27 | } 28 | }; 29 | vector edges; // 邻接表存边 30 | 31 | inline void init(int n) 32 | { 33 | for (int i = 1; i <= n; i++) 34 | fa[i] = i; 35 | } 36 | 37 | int find(int x) 38 | { 39 | return x == fa[x] ? x : (fa[x] = find(fa[x])); 40 | } 41 | 42 | inline void merge(int i, int j) 43 | { 44 | fa[find(i)] = find(j); 45 | } 46 | 47 | int kruskal() 48 | { 49 | sort(edges.begin(), edges.end()); 50 | int ans = 0, selected = 0; 51 | bool flag = false; 52 | for (auto ed : edges) 53 | { 54 | if (find(ed.start) != find(ed.end)) 55 | { 56 | merge(ed.start, ed.end); 57 | ans += ed.dist; 58 | if (++selected == v - 1) 59 | { 60 | flag = true; 61 | break; 62 | } 63 | } 64 | } 65 | return flag ? ans : INF; 66 | } 67 | ``` 68 | 69 | ## 分析 70 | 71 | 该算法的核心思想就是:选择 $A$ 的边集合外,权值最小且不会成环的边,加入到 $A$ 中。 72 | 73 | 实现“选择 $A$ 的边集合外”的方法:将边排序,从小到大有序选择。 74 | 75 | 实现“权值最小”的方法:将边储存在 `vector` 内,按长度排序(我用的重载运算符和 `sort()` ) 76 | 77 | 实现“不会成环”的方法:判断两节点是否已经联通,若已经联通,再加一条边肯定会成环,只有不连通才能合并(我用的并查集)。 78 | 79 | 下面用例子模拟一下该算法的过程: 80 | 81 | **初始化并查集:** 82 | 83 | 将每个节点指向自身,图中用不同颜色表示。 84 | 85 | 86 | 87 | **找到最短的边 $1\leftrightarrow 3$,长度为 $1$:** 88 | 89 | $fa[1]\neq fa[3]$,不会成环。将 $1$、$3$ 节点合并。 90 | 91 | 92 | 93 | **找到最短的边 $4\leftrightarrow 6$,长度为 $2$:** 94 | 95 | $fa[4]\neq fa[6]$,不会成环。将 $4$、$6$ 节点合并。 96 | 97 | 98 | 99 | **找到最短的边 $2\leftrightarrow 5$,长度为 $3$:** 100 | 101 | $fa[2]\neq fa[5]$,不会成环。将 $2$、$5$ 节点合并。 102 | 103 | 104 | 105 | **找到最短的边 $3\leftrightarrow 6$,长度为 $4$:** 106 | 107 | $fa[3]\neq fa[6]$,不会成环。将 $3$、$6$ 节点合并。 108 | 109 | 110 | 111 | **找到最短的边 $1\leftrightarrow 4, 3\leftrightarrow 4, 2\leftrightarrow 3$,长度为 $5$:** 112 | 113 | $fa[1]=fa[4], fa[3]=fa[4]$,会成环。不可合并。 114 | 115 | $fa[2]\neq fa[3]$,不会成环。将 $2$、$3$ 节点合并。 116 | 117 | 118 | 119 | **这时已经求出了最小生成树,后续操作均会成环,因此都会被跳过。** -------------------------------------------------------------------------------- /060~069/067【数论】扩展欧几里得算法.md: -------------------------------------------------------------------------------- 1 | **扩展欧几里得算法**:已知整数 $a$、$b$,求得 $x$、$y$ 满足 $ax+by=\gcd(a,b)$. 2 | 3 | 4 | 5 | # 预备知识 6 | 7 | ### 欧几里得算法 (Euclidean algorithm) 8 | 9 | 常叫辗转相除法,其证明了 $\gcd(a,b)=\gcd(b,a\bmod b)$. 10 | 11 | 这里给出代码供下文参考: 12 | 13 | ```cpp 14 | int gcd(int a, int b) 15 | { 16 | if (!b) 17 | return a; 18 | return gcd(b, a % b); 19 | } 20 | ``` 21 | 22 | 笔记链接:https://io.zouht.com/17.html 23 | 24 | ### 裴蜀定理 (Bézout's lemma) 25 | 26 | 对任何整数 $a$、$b$ 和 $m$,关于未知数 $x$ 和 $y$ 的线性丢番图方程(称为裴蜀等式): 27 | 28 | $$ 29 | ax+by=m 30 | $$ 31 | 有整数解时当且仅当 $m$ 是 $a$ 及 $b$ 的最大公约数 $d$ 的倍数。 32 | 33 | # 扩展欧几里得算法 (Extended Euclidean algorithm) 34 | 35 | ### 算法描述 36 | 37 | 已知整数 $a$、$b$,扩展欧几里得算法可以在求得 $a$、$b$ 的最大公约数的同时,能找到整数 $x$、$y$(其中一个很可能是负数),使它们满足贝祖等式: 38 | $$ 39 | ax + by = \gcd(a, b) 40 | $$ 41 | 42 | ### 算法过程 43 | 44 | **特解** 45 | 46 | 在欧几里得算法中,递归边界时 $b=0$,$\gcd(a,0)=a$。此时裴蜀等式为: 47 | $$ 48 | ax+0y=a 49 | $$ 50 | 易得 $x=1,y=0$,然后返回 $(a,1,0)$,第一位代表最大公约数,第二、三位代表该层裴蜀等式的 $x$ 和 $y$. 51 | 52 | 在递归中间部分时,我们得到了 $\gcd(b,a\bmod b)=d,x,y$。此时裴蜀等式为(不妨令前面系数为 $y$,后面为 $x$): 53 | $$ 54 | \begin{align} 55 | by+(a\bmod b)x&=d\\ 56 | by+(a-\lfloor\frac{a}{b}\rfloor\cdot b)x&=d\\ 57 | ax+b(y-\lfloor\frac{a}{b}\rfloor\cdot x)&=d 58 | \end{align} 59 | $$ 60 | 因此当层的解为: 61 | $$ 62 | \begin{cases} 63 | x'=x\\ 64 | y'=y-\lfloor a/b\rfloor\cdot x\\ 65 | \end{cases} 66 | $$ 67 | 于是返回 $(d,x',y')$. 68 | 69 | **通解** 70 | 71 | 该算法求出的是一对满足条件的 $x_0$ 和 $y_0$,实际上答案不唯一,可以通过构造: 72 | $$ 73 | a(x_0-k\cdot\frac{b}{d})+b(y_0+k\cdot\frac{a}{d})=d\ (k\in\mathbb{Z}) 74 | $$ 75 | 得到通解: 76 | $$ 77 | \begin{cases} 78 | x=x_0-k\cdot(b/d)\\ 79 | y=y_0+k\cdot(a/d)\\ 80 | \end{cases} 81 | $$ 82 | 83 | 84 | ### 代码实现 85 | 86 | 代码中,返回值并未用三元组,而是返回最大公约数,同时通过引用传回了 $x$ 和 $y$,没有本质区别。 87 | 88 | ```cpp 89 | int exgcd(int a, int b, int &x, int &y) 90 | { 91 | if (!b) 92 | { 93 | x = 1; 94 | y = 0; 95 | return a; 96 | } 97 | int d = exgcd(b, a % b, y, x); 98 | y -= a / b * x; 99 | return d; 100 | } 101 | ``` 102 | 103 | # 解决线性同余方程 104 | 105 | 扩展欧几里得算法可以解决形如 $ax\equiv b\pmod m$ 的线性同余方程,方式如下。 106 | 107 | 由 $ax\equiv b\pmod m$ 可知,存在 $y$,使 $ax=b+my$. 不妨将 $my$ 移动至等式左侧,负号放入 $y$ 中,则得到: 108 | $$ 109 | ax+my=b 110 | $$ 111 | 再令 $d=\gcd(a,m)$,那么关于未知数 $x'$ 和 $y'$ 的裴蜀等式可写为: 112 | $$ 113 | ax'+by'=d 114 | $$ 115 | 可使用扩展欧几里得算法得到 $x',y',d$ 的值。如果 $b$ 是 $d$ 的倍数,那么原线性同余方程有解,解为: 116 | $$ 117 | \begin{cases} 118 | x=x'\cdot(b/d)\bmod m\\ 119 | y=y'\cdot(b/d)\bmod m\\ 120 | \end{cases} 121 | $$ 122 | 123 | # 解决模数非质数的乘法逆元 124 | 125 | 当 $p$ 为质数时,可使用费马小定理求出乘法逆元: 126 | $$ 127 | a^{-1}\equiv a^{p-2}\pmod p 128 | $$ 129 | 但当 $p$ 非质数时,该方法失效。此时可构造线性同余方程: 130 | $$ 131 | ax\equiv1\pmod p 132 | $$ 133 | 再使用上一节的方法,利用扩展欧几里得算法解该方程即可,代码如下: 134 | 135 | ```cpp 136 | int inv(int a, int p) 137 | { 138 | int x, y; 139 | if (exgcd(a, p, x, y) != 1) 140 | return -1; // 无解 141 | return (x % p + p) % p; 142 | } 143 | ``` 144 | 145 | 注意无解的情况,由裴蜀定理的约束条件,$a$ 和 $x$ 必须满足 $\gcd(a,p)=1$ 才有解。 146 | 147 | **即:数 $a$ 与模数 $p$ 不互质时,$a$ 对于 $p$ 没有乘法逆元。** 148 | -------------------------------------------------------------------------------- /020~029/025【数论】算术基本定理.md: -------------------------------------------------------------------------------- 1 | 算术基本定理(唯一分解定理):任何一个大于 $1$ 的自然数 $N$,如果 $N$ 不为质数,那么 $N$ 可以唯一分解成有限个质数的乘积 $N=P_1^{a_1}P_2^{a_2}P_3^{a_3}\cdots P_n^{a_n}$,$P_1 4 | 5 | ## 定理推论 6 | 7 | - 一个大于 $1$ 的整数 $N$,如果它的标准分解式为 $N=P_1^{a_1}P_2^{a_2}P_3^{a_3}\cdots P_n^{a_n}$,那么它的正因数个数为 $\sigma_0(N)=(1+a_1)(1+a_2)\cdots(1+a_n)$ 8 | 9 | **简单证明:** 10 | 11 | $P_1^{b_1}P_2^{b_2}P_3^{b_3}\cdots P_n^{b_n}\ (0\leq b_1\leq a_1,0\leq b_2\leq a_2,\cdots)$ 一定是 $N$ 的正因数。其中 $b_1$ 可选 $[0,a_1]$ 共 $1+a_1$ 种,$b_2$ 可选 $[0,a_2]$ 共 $1+a_2$ 种 $\cdots\cdots$ 12 | 13 | 根据乘法原理,整体选法总数为:$(1+a_1)(1+a_2)\cdots(1+a_n)$。每一种选法对应一个正因数,则选法数量就是正因数的个数。 14 | 15 | - 它的全体正因数之和为 $\sigma_1(N)=(P_1^0+P_1^1+P_1^2+\cdots+P_1^{a_1})(P_2^0+P_2^1+P_2^2+\cdots+P_2^{a_2})\cdots(P_n^0+P_n^1+P_n^2+\cdots+P_n^{a_n})$ 16 | 17 | **简单证明:** 18 | 19 | 我们将 $(P_1^0+P_1^1+P_1^2+\cdots+P_1^{a_1})(P_2^0+P_2^1+P_2^2+\cdots+P_2^{a_2})\cdots(P_n^0+P_n^1+P_n^2+\cdots+P_n^{a_n})$ 使用分配律展开,会得到一个多项式。 20 | 21 | 多项式中每个单项式的结构均为 $P_1^{b_1}P_2^{b_2}P_3^{b_3}\cdots P_n^{b_n}\ (0\leq b_1\leq a_1,0\leq b_2\leq a_2,\cdots)$,即 $N$ 的一个正因数。多项式的值便是所有正因数的和。 22 | 23 | ## 常见题型 24 | 25 | #### 求正因数个数 26 | 27 | 需要先用到欧拉筛生成质数,然后用质数从小到大分解 $N$ 得到 $a_1,a_2,a_3,\cdots,a_n$,然后计算出正因数个数 $\sigma_0(N)=(1+a_1)(1+a_2)\cdots(1+a_n)$ 28 | 29 | ```cpp 30 | bool is_prime[SIZE]; 31 | int prime[SIZE], primesize = 0; 32 | // 欧拉筛生成质数代码省略 33 | int fact_cnt(int n) 34 | { 35 | int now = n, ans = 1; 36 | for (int i = 0; i < primesize && prime[i] <= sqrt(now); i++) 37 | { 38 | int cnt = 0; 39 | while (now % prime[i] == 0) 40 | { 41 | cnt++; 42 | now /= prime[i]; 43 | } 44 | ans *= (cnt + 1); 45 | } 46 | if (now != 1) 47 | ans *= 1 + 1; 48 | return ans; 49 | } 50 | ``` 51 | 52 | #### 判断正因数之和奇偶性 53 | 54 | 需要以下结论: 55 | 56 | - 奇数个奇数相加为奇数,偶数个奇数相加为偶数 57 | - 奇数乘奇数为奇数,奇数乘偶数为偶数,偶数乘偶数为偶数 58 | - 除了 $2$,质数均为奇数。因此除了 $2$,质数的任意次幂都是奇数。 59 | 60 | 假设 $\sigma_1(N)=(1+P_1+P_1^2+\cdots+P_1^{a_1})(1+P_2+P_2^2+\cdots+P_2^{a_2})\cdots(1+P_n+P_n^2+\cdots+P_n^{a_n})$ 为奇数,那么: 61 | 62 | **情况 1:**若 $2\mid N$,那么 $P_1=2$,所以 $P_1+P_1^2+\cdots+P_1^{a_1}$ 为偶数,$1+P_1+P_1^2+\cdots+P_1^{a_1}$ 为奇数。 63 | 64 | 如果 $\sigma_1(N)$ 为奇数,那么 $(1+P_2+P_2^2+\cdots+P_2^{a_2}),\cdots,(1+P_n+P_n^2+\cdots+P_n^{a_n})$ 均为奇数 65 | 66 | 即 $(P_2+P_2^2+\cdots+P_2^{a_2}),\cdots,(P_n+P_n^2+\cdots+P_n^{a_n})$ 均为偶数 67 | 68 | 因为 $P_2,\cdots,P_n$ 均为质数,由结论可知,一定是偶数个奇数相加,即 $a_2,\cdots,a_n$ 均为偶数 69 | 70 | **情况 1.1:**若 $a_1$ 为偶数,$N=P_1^{a_1}\cdot P_2^{a_2}\cdot P_3^{a_3}\cdots P_n^{a_n}=(P_1^{a_1/2}\cdot P_2^{a_2/2}\cdot P_3^{a_3/2}\cdots P_n^{a_n/2})^2$ 为完全平方数 $x^2$ 71 | 72 | **情况 1.2:**若 $a_1$ 为奇数, $N=P_1^{a_1}\cdot P_2^{a_2}\cdot P_3^{a_3}\cdots P_n^{a_n}=P_1\times(P_1^{(a_1-1)/2}\cdot P_2^{a_2/2}\cdot P_3^{a_3/2}\cdots P_n^{a_n/2})^2$ 为完全平方数的两倍 $2\cdot x^2$ 73 | 74 | **情况 2:**若 $2\nmid N$,同上得 $(P_1+P_1^2+\cdots+P_1^{a_2}),\cdots,(P_n+P_n^2+\cdots+P_n^{a_n})$ 均为偶数 75 | 76 | 所以 $N=P_1^{a_1}\cdot P_2^{a_2}\cdot P_3^{a_3}\cdots P_n^{a_n}=(P_1^{a_1/2}\cdot P_2^{a_2/2}\cdot P_3^{a_3/2}\cdots P_n^{a_n/2})^2$ 为完全平方数 $x^2$ 77 | 78 | **综上所述:**若 $N=x^2\ 或\ 2\cdot x^2$,$\sigma_1(N)$ 为奇数,反之为偶数。 79 | 80 | 代码实现非常简单,略去。 81 | -------------------------------------------------------------------------------- /001~009/007【算法】二分查找、三分查找.md: -------------------------------------------------------------------------------- 1 | **有序数列查找特定大小数的算法:**二分查找(Binary Search) 2 | 3 | **凸函数或者凹函数寻找极值的算法:**三分查找(Ternary Search) 4 | 5 | 6 | 7 | # 二分查找 8 | 9 | ## 分析 10 | 11 | 以递增数组为例,将其分为两半。若中间值大于要找的数,说明要找的数在左边一半;若中间值小于要找的数,说明要找的数在右边一半。不断将范围缩小一半,直到找到要找的数。 12 | 13 | ## 复杂度 14 | 15 | 时间复杂度:$O(\log n)$ 16 | 17 | ($n$ 为数据规模) 18 | 19 | ## 代码实现 20 | 21 | ### 整数 22 | 23 | - 查找 $\ge x$ 的第一个数: 24 | 25 | (将 `if (a[mid] >= x)` 改为 `if (a[mid] > x)` ,可查找 $> x$ 的第一个数) 26 | 27 | ```cpp 28 | // a[ ]为储存数据的有序递增数组 29 | // l ~ r为二分查找的数组范围 30 | int l = 0, r = n - 1; 31 | while (l < r) 32 | { 33 | int mid = (l + r) / 2; 34 | if (a[mid] >= x) 35 | r = mid; 36 | else 37 | l = mid + 1; 38 | } 39 | ``` 40 | 41 | - 查找 $\le x$ 的最后一个数: 42 | 43 | (将 `if (a[mid] <= x)` 改为 `if (a[mid] < x)` ,查找 $< x$ 的最后一个数) 44 | 45 | 易错点:此时 $mid$ 为 $(l+r+1)/2$,若忘记 $+1$ 将会死循环。 46 | 47 | ```cpp 48 | // a[ ]为储存数据的有序递增数组 49 | // l ~ r为二分查找的数组范围 50 | int l = 0, r = n - 1; 51 | while (l < r) 52 | { 53 | int mid = (l + r + 1) / 2; 54 | if (a[mid] <= x) 55 | l = mid; 56 | else 57 | r = mid - 1; 58 | } 59 | ``` 60 | 61 | ### 实数 62 | 63 | 查找满足一定条件的实数:(即以下代码令 check(x) == true 的 x) 64 | 65 | ```cpp 66 | // check(int x): 判断x是否满足条件 67 | // eps: 精度(因为浮点数误差,不可直接比大小) 68 | // BEGIN: 查找左边界 69 | // END: 查找右边界 70 | bool check(int x); 71 | double eps = 1e-6; 72 | double l = BEGIN, r = END; 73 | while (r - l > eps) 74 | { 75 | double mid = (l + r) / 2; 76 | if (check(mid)) 77 | l = mid; 78 | else 79 | r = mid; 80 | } 81 | ``` 82 | 83 | ### algorithm 内函数 84 | 85 | **在有序递增数组中,查找 $\ge x(或> x)$ 的第一个数:** 86 | 87 | lower_bound(begin, end, num): 从数组的 begin 位置到 end - 1 位置二分查找第一个大于或等于 num 的数字,找到返回该数字的地址,不存在则返回 end。 88 | 89 | upper_bound(begin, end, num): 从数组的 begin 位置到 end - 1 位置二分查找第一个大于 num 的数字,找到返回该数字的地址,不存在则返回 end。 90 | 91 | **在有序递减数组中,查找 $\le x(或< x)$ 的第一个数:** 92 | 93 | lower_bound(begin, end, num, greater\() ): 从数组的 begin 位置到 end - 1 位置二分查找第一个小于或等于 num 的数字,找到返回该数字的地址,不存在则返回 end。 94 | 95 | upper_bound(begin, end, num, greater\() ): 从数组的 begin 位置到 end - 1 位置二分查找第一个小于 num 的数字,找到返回该数字的地址,不存在则返回 end。 96 | 97 | **将返回的地址减去起始地址,得到数字在数组中的下标:** 98 | 99 | `int position = lower_bound(num, num + 100, 64) - num;` 100 | 101 | 这样,`num[position]` 就是找到的第一个大于等于 64 的数字。 102 | 103 | # 三分查找 104 | 105 | ## 分析 106 | 107 | 将 l ~ r 区间分为三份。如果 $f(m_1)f(m_2)$,则极值点一定在区间 $(m_1,r)$. 108 | 109 | (只适用于区间内为单峰的函数) 110 | 111 | 112 | 113 | ## 时间复杂度 114 | 115 | 时间复杂度:$O(\log n)$ 116 | 117 | ($n$ 为数据规模) 118 | 119 | ## 代码实现 120 | 121 | ```cpp 122 | // f(double x): 给定区间内的凹函数或凸函数 123 | // eps: 精度(因为浮点数误差,不可直接比大小) 124 | // BEGIN: 查找左边界 125 | // END: 查找右边界 126 | double f(double x); 127 | double eps = 1e-6; 128 | double l = BEGIN, r = END; 129 | while(r - l > eps) 130 | { 131 | double m1 = l + (r - l) / 3, m2 = r - (r - l) / 3; 132 | if(f(m1) > f(m2)) // 此为找最小值,若要找最大值,则改为< 133 | l = m1; 134 | else 135 | r = m2; 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /010~019/010【题目】满意的集合.md: -------------------------------------------------------------------------------- 1 | **牛客小白月赛43** 2 | 3 | E - 满意的集合 4 | 5 | https://ac.nowcoder.com/acm/contest/11220/E 6 | 7 | 8 | 9 | 时间限制:C/C++ 1秒,其他语言2秒 10 | 空间限制:C/C++ 262144K,其他语言524288K 11 | 64bit IO Format: %lld 12 | 13 | ## 题目描述 14 | 15 | 有数字 $1\sim9$,每个数字的个数分别为 $cnt_1,cnt_2,cnt_3,...,cnt_9$ 。计算出“满意的集合“的个数。 16 | 17 | "满意的集合" 定义:选出的数存在一种排列方式,其拼接起来后表示的**十进制**整数,能被 $3$ 整除,例如集合 $\{3,3,6\}$ $($包含了 $2$ 个数字 $3,1$ 个数字 $6$ $)$,可以有排列 $\{6,3,3\}$ 代表十进制下的整数 $633$,能被 $3$ 整除。 18 | 19 | 两个集合相同,当且仅当集合元素个数相同,且排序后对应数字相同,例如 $\{3,3,6\}$ 和 $\{3,6,3\}$ 是同样的集合。 20 | 21 | 空集合看作 $0$ ,是合法的,答案对 $1e9+7$ 取模。 22 | 23 | ## 输入描述: 24 | 25 | > 输入一行,包括 $9$ 个整数 $cnt_1,cnt_2,cnt_3,...,cnt_9$,分别表示数字 $1\sim9$ 的个数,$0\le cnt_i\le 1e9$。 26 | 27 | ## 输出描述: 28 | 29 | > 输出一行,表示”满意的集合”的个数,答案对 $1e9+7$ 取模。 30 | 31 | ## 示例1 32 | 33 | ### 输入 34 | 35 | > 1 1 1 0 0 0 0 0 0 36 | 37 | ### 输出 38 | 39 | > 4 40 | 41 | ### 说明 42 | 43 | > 全部的数字集合 $\{1,2,3\}$。可能的集合有$\{\},\{1\},\{2\},\{3\},\{1,2\},\{1,3\},\{2,3\},\{1,2,3\}$。其中“满意的集合“有 $\{1,2\},\{3\},\{1,2,3\},\{\}$ 共 $4$ 个。 44 | 45 | ## 我的笔记 46 | 47 | ### 分析 48 | 49 | 首先简化题意,将“排列 $\{6,3,3\}$ 代表十进制下的整数 $633$,能被 $3$ 整除”,转化为“$\{6,3,3\}$ 中元素每一位的和能被 $3$ 整除”。 50 | 51 | 然后进行分析,本题可使用动态规划算法,将其转化为递推。递推第 0 层就是数字集合里不选任何数的情况,第 1 层就是数字集合里只选 1 的情况,第 2 层就是数字集合里只选 1、2 的情况,第 3 层就是数字集合里只选 1、2、3 的情况……(每层递推都允许不选任何数) 52 | 53 | 然后得到以下 DP 写法: 54 | 55 | ```cpp 56 | // TLE Version DP 57 | for (int i = 1; i <= 9; i++) // 递推层数,直到推到第9层,即获得符合题意的答案 58 | { 59 | for (int j = 0; j <= cnt[i]; j++) // 遍历选0个i、1个i、2个i……直到全选的所有情况 60 | { 61 | for (int k = 0; k < 3; k++) // 递推模为0、为1、为2的结果 62 | { 63 | dp[i][(j * i + k) % 3] += dp[i - 1][k]; 64 | dp[i][(j * i + k) % 3] %= MOD; // MOD为1e9+7 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | 但可以发现,该方法时间复杂度极高,为 $O(\sum_{n=1}^9cnt_n)$ ,一定会 TLE 71 | 72 | 进一步分析,每个数选 $k,k+3,k+6,\cdots,k+3*x(k\in\{0,1,2\})$ 时,和与 3 的模数是相等的。如: 73 | 74 | (0 \* 2) % 3 = 0, (1 \* 2) % 3 = 2, (2 \* 2) % 3 = 1, 75 | (3 \* 2) % 3 = 0, (4 \* 2) % 3 = 2, (5 \* 2) % 3 = 1…… 76 | 77 | 这样我们就能把 $cnt_1$ 种情况转化为 3 种情况,即选 0 个、选 1 个、选 2 个,然后剩余情况都与这三个相等,我们统计一下这三种情况的个数,修改掉上面的 DP 中的第二层循环,就能将时间复杂度降低至 $O(9*3*3)$ 78 | 79 | ### 源码 80 | 81 | ```cpp 82 | #include 83 | #define MOD 1000000007 84 | 85 | using namespace std; 86 | 87 | int main(void) 88 | { 89 | int cnt[10]; 90 | for (int i = 1; i <= 9; i++) 91 | { 92 | cin >> cnt[i]; 93 | } 94 | long long dp[10][3]; 95 | memset(dp, 0, sizeof(dp)); 96 | dp[0][0] = 1; 97 | for (int i = 1; i <= 9; i++) 98 | { 99 | int mod_1 = (0 * i) % 3, mod_2 = (1 * i) % 3, mod_3 = (2 * i) % 3; 100 | int cnt_mod_1 = cnt[i] / 3 + 1, cnt_mod_2 = (cnt[i] + 1) / 3, cnt_mod_3 = (cnt[i] + 2) / 3; 101 | for (int j = 0; j < 3; j++) 102 | { 103 | dp[i][(j + mod_1) % 3] += dp[i - 1][j] * cnt_mod_1; 104 | dp[i][(j + mod_2) % 3] += dp[i - 1][j] * cnt_mod_2; 105 | dp[i][(j + mod_3) % 3] += dp[i - 1][j] * cnt_mod_3; 106 | dp[i][(j + mod_1) % 3] %= MOD; 107 | dp[i][(j + mod_2) % 3] %= MOD; 108 | dp[i][(j + mod_3) % 3] %= MOD; 109 | } 110 | } 111 | cout << dp[9][0] << endl; 112 | return 0; 113 | } 114 | ``` 115 | 116 | -------------------------------------------------------------------------------- /140~149/148【题目】Agnej.md: -------------------------------------------------------------------------------- 1 | **“范式杯”2023牛客暑期多校训练营10** 2 | 3 | D - Agnej 4 | 5 | https://ac.nowcoder.com/acm/contest/57364/D 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 有个 $n+1$ 层的木塔,每层原本由 $m$ 根木头构成。现在除了最顶层是满的 $m$ 根木头,其余每层有些木头已经被抽走了。先后手轮流从非顶层抽走一根木头,如果一层中最左侧或最右侧 $\lceil\frac{m}{2}\rceil$ 根木头都被抽走,则木塔倒塌。谁让木塔倒塌谁输,问胜负。$1\leq n\times m\leq 10^5$。 12 | 13 | ## 题解 14 | 15 | 显然每一层独立,因此每一层都是一个有向图游戏,那么根据 SG 定理,对每一个层求出 SG 值再异或到一起,如果答案 $\neq0$ 则先手必胜,$0$ 则先手必败。下面仅考虑求解一层的 SG 值。 16 | 17 | #### $m$ 为偶数 18 | 19 | 为方便,记左侧有 $l$ 块,右侧有 $r$ 块。 20 | 21 | 要求 $SG(l,r)$,则考察其子情况。考察到左右仅一块的边界情况,此时先手必败,即 $SG(1,1)=0$. 22 | 23 | 每经过一次传递 SG 值会 $0/1$ 切换,那么 $SG(l,r)=(l+r)\bmod2$. 24 | 25 | #### $m$ 为奇数 26 | 27 | 奇数情况的唯一特殊点就是最中心的一块。 28 | 29 | 为方便,中心有 $c$ 块($c=0/1$),记中心左侧有 $l$ 块,右侧有 $r$ 块。且下面的分类讨论中,不妨默认 $l$ 取特殊值。 30 | 31 | 1. 当 $c=0$ 时,退化为偶数情况,答案 $SG(l,0,r)=(l+r)\bmod2$. 32 | 33 | 2. 当 $c=1$ 时,需要分类讨论: 34 | 35 | 1. 有一侧为空 $SG(0,1,r)$ 时,考虑边界情况 $SG(0,1,0)$: 36 | 37 | - $SG(0,1,0)=0$,每经过一次传递 SG 值会 $0/1$ 切换,那么 $SG(0,1,r)=r\bmod2$. 38 | 39 | 2. 有一侧仅一块 $SG(1,1,r)$ 时,考虑边界情况 $SG(1,1,1)$: 40 | 41 | 1. 抽中间一块,转移为情况 $1$,SG 值 $(1+r)\bmod2$. 42 | 2. 抽 $=1$ 的一侧,转移为情况 $2.1$,SG 值 $r\bmod 2$. 43 | 44 | - 情况 $2.2.1$ 和 $2.2.2$ 的 SG 值一定包含 $0$ 和 $1$,则 $SG(1,1,1)=2$,那么每经过一次传递 SG 值会 $2/3$ 切换,那么 $SG(1,1,r)=2+(r+1)\bmod 2$. 45 | 46 | 3. 一般情况 $SG(l,1,r)$ 时,考虑边界情况 $SG(2,1,r)$: 47 | 48 | 1. 抽中间一块,转移为情况 $1$, SG 值 $r\bmod2$. 49 | 50 | 2. 抽左侧一块,转移为情况 $2.2$,SG 值为 $2+(r+1)\bmod 2$ 51 | 52 | 3. 抽右侧一块,由于 $SG(2,1,1)=3$,考虑边界情况 $SG(2,1,2)=1$,$SG(2,1,3)=0$,$\dots$ 53 | 54 | - 经过归纳得到,$SG(2,1,r)=(l+1+r)\bmod 2$. 55 | 56 | ## 代码 57 | 58 | ```cpp 59 | #include 60 | #define endl '\n' 61 | // #define int long long 62 | 63 | using namespace std; 64 | 65 | int sg(string &s) 66 | { 67 | int len = s.size(); 68 | int mid = len / 2; 69 | int cnt_l = 0, cnt_r = 0; 70 | for (int i = 0; i <= mid - 1; i++) 71 | if (s[i] == '1') 72 | cnt_l++; 73 | for (int i = mid; i < len; i++) 74 | if (s[i] == '1') 75 | cnt_r++; 76 | if (len % 2) 77 | { 78 | cnt_r -= s[mid] - '0'; 79 | if (s[mid] == '0') 80 | return (cnt_l + cnt_r) % 2; 81 | if (cnt_l == 0 || cnt_r == 0) 82 | return max(cnt_l, cnt_r) % 2; 83 | if (cnt_l == 1 || cnt_r == 1) 84 | return 2 + (max(cnt_l, cnt_r) + 1) % 2; 85 | return (cnt_l + cnt_r + 1) % 2; 86 | } 87 | return (cnt_l + cnt_r) % 2; 88 | } 89 | 90 | void solve() 91 | { 92 | int n, m; 93 | cin >> n >> m; 94 | int ans = 0; 95 | while (n--) 96 | { 97 | string s; 98 | cin >> s; 99 | ans ^= sg(s); 100 | } 101 | if (ans) 102 | cout << "Alice" << endl; 103 | else 104 | cout << "Bob" << endl; 105 | } 106 | 107 | signed main() 108 | { 109 | ios::sync_with_stdio(false); 110 | cin.tie(0); 111 | cout.tie(0); 112 | int t = 1; 113 | // cin >> t; 114 | while (t--) 115 | solve(); 116 | return 0; 117 | } 118 | ``` 119 | 120 | -------------------------------------------------------------------------------- /140~149/149【题目】IUPC.md: -------------------------------------------------------------------------------- 1 | **“范式杯”2023牛客暑期多校训练营10** 2 | 3 | F - IUPC 4 | 5 | https://ac.nowcoder.com/acm/contest/57364/F 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 一场时长为 $t$ 分钟的比赛有 $n$ 道题,每个题有最早过题时间的约束,并且一分钟内至多只能交一题,任意 $k$ 分钟内只能至多交 $m$ 题,问比赛 AK 的方案数。 $1\leq t\leq 300$,$1\leq n \leq 13$ ,$1\leq m\leq k\leq 10$。 12 | 13 | ## 题解 14 | 15 | 使用状态压缩动态规划思想。 16 | 17 | #### 状态表示 18 | 19 | $dp_{i,j,S}:=$ 在 $1\sim i$ 分钟,提交了 $j$ 题,且最后 $k$ 分钟的提交情况为 $S$ 的方案数。 20 | 21 | 其中提交情况 $S$ 是一个 $k$ 位二进制数:${\overbrace{00\dots00}^{k\ 位}}_2$,其中从左到右第 $p$ 位代表第 $i-k+p$ 时刻是否过题,如果过题则为 $1$,反之为 $0$. 22 | 23 | #### 状态计算 24 | 25 | 首先需要预处理每个 $i$ 时刻有答案可提交的题目数 $pa_i$,用一个前缀和处理即可。那么 $i$ 时刻可以提交的题目有 $pa_i-j$ 种,即有答案可提交的数目减去之前已经提交过的数目。 26 | 27 | 为方便表示,定义 $\mathrm{LeftShift}(S):=$ $S$ 左移一位,且去除超出 $k$ 位的部分(保证此时仍然为 $k$ 位),定义 $\mathrm{PopCount}(S):=$ $S$ 的二进制表示中 $1$ 的数目。 28 | 29 | - 如果第 $i$ 时刻不提交,转移到的情况 $C=\mathrm{LeftShift}(S)$,情况数直接原封不动加进去: 30 | 31 | $$ 32 | dp_{i,j,C}=dp_{i,j,C}+dp_{i-1,j,S} 33 | $$ 34 | 35 | - 如果第 $i$ 时刻提交,转移到的情况 $C=\mathrm{LeftShift}(S)+1$,需要分类讨论: 36 | - 如果 $\mathrm{PopCount}(C)>m$,则 $k$ 分钟内交题数超限,这种转移不成立。 37 | - 如果 $\mathrm{PopCount}(C)\leq m$,则可以正常交题,可提交的题目种类有 $pa_i-j$ 种,转移方式: 38 | 39 | $$ 40 | dp_{i,j+1,C}=dp_{i,j+1,C}+(pa_i-j)\cdot dp_{i-1,j,S} 41 | $$ 42 | 43 | 最后答案就是: 44 | $$ 45 | \sum_{S=0}^{2^k-1}dp_{t,n,S} 46 | $$ 47 | 48 | ## 代码 49 | 50 | ```cpp 51 | #include 52 | #define endl '\n' 53 | #define int long long 54 | 55 | using namespace std; 56 | 57 | constexpr int MAXN = 310, MAXM = 15, MOD = 1e9 + 7; 58 | int n, t, k, m; 59 | int a[MAXN], pa[MAXN]; 60 | int dp[MAXN][MAXM][1 << MAXM]; 61 | 62 | void solve() 63 | { 64 | cin >> n >> t >> k >> m; 65 | for (int i = 1; i <= n; i++) 66 | { 67 | int t; 68 | cin >> t; 69 | a[t]++; 70 | } 71 | for (int i = 1; i <= t; i++) 72 | pa[i] = pa[i - 1] + a[i]; 73 | int mask = (1 << k) - 1; 74 | dp[0][0][0] = 1; 75 | for (int i = 1; i <= t; i++) 76 | { 77 | for (int j = 0; j <= pa[i]; j++) 78 | { 79 | for (int p = 0; p < (1 << k); p++) 80 | { 81 | int cur = (p << 1) & mask; 82 | dp[i][j][cur] += dp[i - 1][j][p]; 83 | dp[i][j][cur] %= MOD; 84 | } 85 | } 86 | for (int j = 0; j <= pa[i]; j++) 87 | { 88 | for (int p = 0; p < (1 << k); p++) 89 | { 90 | int cur = ((p << 1) & mask) | 1; 91 | if (__builtin_popcount(cur) > m) 92 | continue; 93 | dp[i][j + 1][cur] += dp[i - 1][j][p] * (pa[i] - j) % MOD; 94 | dp[i][j + 1][cur] %= MOD; 95 | } 96 | } 97 | } 98 | int ans = 0; 99 | for (int i = 0; i < (1 << k); i++) 100 | ans = (ans + dp[t][n][i]) % MOD; 101 | cout << ans << endl; 102 | } 103 | 104 | signed main() 105 | { 106 | ios::sync_with_stdio(false); 107 | cin.tie(0); 108 | cout.tie(0); 109 | int t = 1; 110 | // cin >> t; 111 | while (t--) 112 | solve(); 113 | return 0; 114 | } 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /020~029/021【题目】digitnum .md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 238** 2 | 3 | C - digitnum 4 | 5 | https://atcoder.jp/contests/abc238/tasks/abc238_c 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $300$ points 12 | 13 | ### Problem Statement 14 | 15 | Given an integer $N$, solve the following problem. 16 | 17 | Let $f(x)=$ (The number of positive integers at most $x$ with the same number of digits as $x$). 18 | Find $f(1)+f(2)+\dots+f(N)$ modulo $998244353$. 19 | 20 | ### Constraints 21 | 22 | - $N$ is an integer. 23 | - $1 \le N < 10^{18}$ 24 | 25 | ------ 26 | 27 | ### Input 28 | 29 | Input is given from Standard Input in the following format: 30 | 31 | > $N$ 32 | 33 | ### Output 34 | 35 | Print the answer as an integer. 36 | 37 | ------ 38 | 39 | ### Sample Input 1 40 | 41 | > 16 42 | 43 | 44 | ### Sample Output 1 45 | 46 | > 73 47 | 48 | - For a positive integer $x$ between $1$ and $9$, the positive integers at most $x$ with the same number of digits as $x$ are $1,2,\dots,x$. 49 | - Thus, we have $f(1)=1,f(2)=2,...,f(9)=9$. 50 | - For a positive integer $x$ between $10$ and $16$, the positive integers at most $x$ with the same number of digits as $x$ are $10,11,\dots,x$. 51 | - Thus, we have $f(10)=1,f(11)=2,...,f(16)=7$. 52 | 53 | ------ 54 | 55 | ### Sample Input 2 56 | 57 | > 238 58 | 59 | ### Sample Output 2 60 | 61 | > 13870 62 | 63 | ------ 64 | 65 | ### Sample Input 3 66 | 67 | > 999999999999999999 68 | 69 | ### Sample Output 3 70 | 71 | > 762062362 72 | 73 | Be sure to find the sum modulo $998244353$. 74 | 75 | ### 我的笔记 76 | 77 | 这道题很明显是一个逆元题,并没有什么难度,只不过当时完全忘记了除法逆元这回事,于是较大数据全部 WA 78 | 79 | 除法逆元: 80 | 81 | >由费马小定理可推出:$\frac{1}{a} \% m = a^{m-2} \% m$ ,其中m为素数。 82 | > 83 | >那么,$\frac{b}{a} \% m$ 就可以变成 $b\times a^{m-2} \% m$ 84 | > 85 | >如果 $m$ 太大,可以使用快速幂。 86 | 87 | 因为本题只用 /2 需要逆元,因此直接把 2 的逆元求出来然后代入就行了,不需要每次都求一边。写个程序算出来为 499122177 88 | 89 | 解决完逆元问题后我仍然没有得出正确答案,折腾了半天发现中途乘法溢出了 long long 范围。比如 `long long ans = ((1 + N - pow_10(num_digit - 1) + 1) % MOD * (N - pow_10(num_digit - 1) + 1) % MOD * 499122177) % MOD`,三个数都是 1e9 级别的,乘起来肯定溢出了。因此得每乘一次就取一次模。 90 | 91 | ### 源码 92 | 93 | ```cpp 94 | #include 95 | #define MOD 998244353 96 | #define INV_2 499122177 97 | 98 | using namespace std; 99 | 100 | long long pow_10(int x) 101 | { 102 | long long ans = 1; 103 | for (int i = 0; i < x; i++) 104 | ans *= 10; 105 | return ans; 106 | } 107 | 108 | int main(void) 109 | { 110 | long long N; 111 | cin >> N; 112 | long long t = N; 113 | int num_digit = 0; 114 | while (t) 115 | { 116 | t /= 10; 117 | num_digit++; 118 | } 119 | long long x = (N - pow_10(num_digit - 1) + 1) % MOD; 120 | long long ans = x; 121 | ans *= x + 1; 122 | ans %= MOD; 123 | ans *= INV_2; 124 | ans %= MOD; 125 | for (int i = num_digit - 1; i > 0; i--) 126 | { 127 | long long y = (pow_10(i) - pow_10(i - 1)) % MOD; 128 | long long tmp = y; 129 | tmp *= y + 1; 130 | tmp %= MOD; 131 | tmp *= INV_2; 132 | tmp %= MOD; 133 | ans += tmp; 134 | ans %= MOD; 135 | } 136 | cout << ans << endl; 137 | return 0; 138 | } 139 | ``` 140 | 141 | -------------------------------------------------------------------------------- /050~059/054【数据结构】树状数组.md: -------------------------------------------------------------------------------- 1 | **树状数组** (Binary Index Tree, BIT / Fenwick Tree): 对于满足结合律且可差分的问题,可在 $O(\log n)$ 进行单点修改和区间查询的数据结构。 2 | 3 | **满足结合律且可差分的问题**:对于运算 $\circ$,如果其存在逆运算 $\bullet$,即 $x\circ y\bullet y=x$,则该运算是可差分的。若该运算同时满足结合律,则是满足结合律且可差分的问题。符合这个性质的常见运算有 $+,\times,\oplus$. 4 | 5 | 6 | 7 | # 树状数组 8 | 9 | ### 时间复杂度:$O(\log n)$ 10 | 11 | 单点修改和区间查询均为该复杂度。 12 | 13 | ### 分析 14 | 15 | #### 意义 16 | 17 | 首先我们来看两种数组,一种是普通数组,长度为 $n$: 18 | $$ 19 | 1,2,3,4,5,6,7,8,\cdots 20 | $$ 21 | 对于上面这个普通数组,单点修改时间复杂度 $O(1)$,但区间求和的复杂度为 $O(n)$。 22 | 23 | 我们可以用前缀和优化,变成长度为 $n$ 的前缀和数组: 24 | $$ 25 | 1,3,6,10,15,21,28,36,\cdots 26 | $$ 27 | 对于上面这个前缀和数组,区间求和的时间复杂度为 $O(1)$,但单点修改的复杂度为 $O(n)$。 28 | 29 | 可以发现以上两种数组都有长处和短处,但根据木桶效应,要想整体优秀,最好还是各方面平衡。树状数组便是平衡了这两种操作的时间复杂度的数据结构,它将其平均为了 $O(\log n)$。 30 | 31 | #### 原理及代码 32 | 33 | 普通数组单点修改快,是因为每一个元素都是独立的。前缀和数组区间求和快,是因为它的元素就是区间的和。为了平衡,我们可以将其变为一个个独立小区间,这样单点修改时只需要修改小区间,无需整个修改。区间求和时可以直接用小区间求和,也可以少算很多步。这时小区间的排布非常重要,如何才能达到最好的效果。 34 | 35 | **区间排布** 36 | 37 | 树状数组的区间排布用的是二进制思想,就其英文名“二进制下标树”也可以知道这点。其排布如下图所示: 38 | 39 | 40 | 41 | Array 为原数组,BIT 为转化为的树状数组,数组下标均从 $1$ 开始。用二进制来表示树状数组的下标,其第 $i$ 位储存区间 $(i-\mathrm{lowbit}(i),i]$ 的和。其中 $\mathrm{lowbit}(i):=$ 二进制数 $i$ 的最右边的一个 $1$ 连带后面的 $0$. 42 | 43 | 这么说还是不够形象,举几个例子: 44 | 45 | $\mathrm{lowbit}(0111)=1$,$0111-\mathrm{lowbit}(0111)=0110$,那么树状数组第 $0111$ 位储存 $(0110,0111]$ 的和; 46 | 47 | $\mathrm{lowbit}(0100)=100$,$0100-\mathrm{lowbit}(0100)=0000$,那么树状数组第 $0100$ 位储存 $(0000,0100]$ 的和。 48 | 49 | 用通俗易懂的话来说: 50 | 51 | 第 $i$ 位储存的区间,右端点就是 $i$(闭),左端点就是把 $i$ 二进制表示中最靠右的 $1$ 变成 $0$(开) 52 | 53 | ```cpp 54 | const int MAXN = 1e6; 55 | int Fenwick_Tree[MAXN]; 56 | // 如果不是全局变量记得初始化为0 57 | ``` 58 | 59 | **区间求和** 60 | 61 | 例如我们要求区间 $(0000,0111]$ 的和,我们需要将区间 $(0000,0100],(0100,0110],(0110,0111]$ 相加。通过观察可以发现,这就是不断减去 $\mathrm{lowbit}(i)$,到 $0$ 为止。 62 | 63 | 64 | 65 | 此时我们需要用到 $\mathrm{lowbit}(x)$ 这个函数的求法,其实很简单 $\mathrm{lowbit}(x)=(x)\&(-x)$,能这么用主要是因为计算机中有符号整型的储存方式,即用补码来储存,如下面例子: 66 | $$ 67 | \begin{align} 68 | 原码:&010111100\\ 69 | 反码:&101000011\\ 70 | 补码: &101000100\\ 71 | 原码\&补码:&000000100 72 | \end{align} 73 | $$ 74 | 我们知道正数的原码等于补码,那么一个数和自己的相反数做按位与,便是原码和补码做按位与,即可计算出 $\mathrm{lowbit}(x)$. 75 | 76 | ```cpp 77 | // 查询(0,pos]的区间和 78 | int query(int pos) 79 | { 80 | int ans = 0; 81 | for (int i = pos; i; i -= i & -i) 82 | ans += Fenwick_Tree[i]; 83 | return ans; 84 | } 85 | ``` 86 | 87 | 以上计算的是从 $0$ 开始的区间和,如果区间左端不是 $0$,那么可以进行转化,即转化为两个从 $0$ 开始的区间和之差。 88 | 89 | ```cpp 90 | // 查询[l,r]的区间和 91 | inline int query(int l, int r) 92 | { 93 | return query(r) - query(l - 1); 94 | } 95 | ``` 96 | 97 | **单点修改** 98 | 99 | 例如我们要将第 $0011$ 位的值 $+1$,我们需要找到包含该位数据的所有区间,即 $(0010,0011],(0000,0100],(0000,1000]$,然后将这三个区间的值均 $+1$。通过观察可发现,这就是不断加上 $\mathrm{lowbit}(i)$,到最大下标为止。 100 | 101 | 102 | 103 | ```cpp 104 | // 第pos位加上val 105 | void update(int pos, int val) 106 | { 107 | for (int i = pos; i < MAXN; i += i & -i) 108 | Fenwick_Tree[i] += val; 109 | } 110 | ``` 111 | 112 | -------------------------------------------------------------------------------- /150~159/158【题目】新取模运算.md: -------------------------------------------------------------------------------- 1 | **2023 年华中科技大学程序设计竞赛新生赛** 2 | 3 | F - 新取模运算 4 | 5 | https://www.luogu.com.cn/problem/P9774?contestId=138479 6 | 7 | 8 | 9 | ## 题目 10 | 11 | 在这道题中,我们定义一个新的运算符号 $\oplus$ 并将其称为新取模运算。 12 | 13 | 当计算 $x \oplus y$ 时,如果 $x$ 不是 $y$ 的倍数,则得到 $x$ 除以 $y$ 的余数; 否则令 $x$ 不断除以 $y$ 直到 $x$ 不再是 $y$ 的倍数,假设它为 $x'$,然后得到 $x'$ 除以 $y$ 的余数。例如,$4\oplus 5=4$,$20\oplus 5=4$,$100\oplus 5=4$。 14 | 15 | 给定一个质数 $p$,接下来会有多组询问,对于每次询问会给出一个整数 $n$,你需要计算出 $n!\oplus p$ 的值。其中 $n!$ 是 $n$ 的阶乘,即所有小于等于 $n$ 的正整数的乘积。 16 | 17 | ## 输入 18 | 19 | 第一行包含两个整数 $T\ (1\le T\le 10^5)$ 和 $p\ (2\le p\le 10^6)$,分别表示询问的次数和给定的质数。 20 | 21 | 接下来 $T$ 行,每行包含一个整数 $n\ (1\le n\le 10^{18})$,含义如题目所述。 22 | 23 | ## 输出 24 | 25 | 对于每次询问,输出一行包含一个整数,即 $n!\oplus p$ 的值。 26 | 27 | ## 样例 28 | 29 | 样例输入 #1 30 | 31 | > 3 7 32 | > 11 33 | > 45 34 | > 14 35 | 36 | 样例输出 #1 37 | 38 | > 4 39 | > 1 40 | > 2 41 | 42 | 样例输入 #2 43 | 44 | > 2 10007 45 | > 1919 46 | > 810 47 | 48 | 样例输出 #2 49 | 50 | > 3152 51 | > 3679 52 | 53 | ## 题解 54 | 55 | 根据取模的性质: 56 | $$ 57 | (a\times b)\bmod p=(a\bmod p)\times(b\bmod p)\bmod p 58 | $$ 59 | 那么对于 $n!\oplus p$,就可以把 $0\sim n$ 堆叠起来: 60 | 61 | | 模 $p$ 为 $0$ | 模 $p$ 为 $1$ | $\dots$ | 模 $p$ 为 $p - 1$ | 62 | | ------------- | ------------- | --------- | ----------------- | 63 | | $0$ | $1$ | $\dots$ | $p - 1$ | 64 | | $p$ | $p + 1$ | $\dots$ | $2p - 1$ | 65 | | $2p$ | $2p + 1$ | $\dots$ | $3p - 1$ | 66 | | $\vdots$ | $\vdots$ | | $\vdots$ | 67 | | $(k - 1) p$ | $(k-1) p + 1$ | $\dots$ | $kp - 1$ | 68 | | $kp$ | $kp+1$ | $\dots n$ | | 69 | 70 | 其中,$k$ 的具体值可以计算出来为 $\lfloor\frac{n}{p}\rfloor$ 71 | 72 | 我们分块来考虑: 73 | 74 | - 除最左侧一列外的剩余部分 75 | - 除最下面一行外的剩余部分:$[(p-1)!]^{k}$ 76 | - 最下面的一行:$(n\bmod p)!$ 77 | - 最左侧的一列 78 | - 将 $p$ 除掉后剩余内容为 $k!$,递归进行计算。 79 | 80 | 预处理出 $1\sim p-1$ 的阶乘值,再使用快速幂计算幂次,复杂度为 $O(p+\log n)$ 可以接受 81 | 82 | ## 代码 83 | 84 | ```cpp 85 | #include 86 | #define endl '\n' 87 | #define int long long 88 | 89 | using namespace std; 90 | 91 | constexpr int MAXN = 1e6 + 10; 92 | int fact[MAXN]; 93 | int n, p; 94 | 95 | int bpow(int a, int b) 96 | { 97 | a %= p; 98 | int ans = 1; 99 | while (b) 100 | { 101 | if (b % 2) 102 | ans = ans * a % p; 103 | a = a * a % p; 104 | b /= 2; 105 | } 106 | return ans; 107 | } 108 | 109 | void init_fact() 110 | { 111 | fact[0] = fact[1] = 1; 112 | for (int i = 2; i < p; i++) 113 | fact[i] = fact[i - 1] * i % p; 114 | } 115 | 116 | int calc(int n) 117 | { 118 | if (n == 0) 119 | return 1; 120 | int ans = bpow(fact[p - 1], n / p) * fact[n % p] % p; 121 | return calc(n / p) * ans % p; 122 | } 123 | 124 | void solve() 125 | { 126 | cin >> n; 127 | cout << calc(n) << endl; 128 | } 129 | 130 | signed main() 131 | { 132 | ios::sync_with_stdio(false); 133 | cin.tie(0); 134 | cout.tie(0); 135 | int t = 1; 136 | cin >> t >> p; 137 | init_fact(); 138 | while (t--) 139 | solve(); 140 | return 0; 141 | } 142 | ``` 143 | 144 | -------------------------------------------------------------------------------- /080~089/084【题目】最短汉密尔顿路径.md: -------------------------------------------------------------------------------- 1 | **最短 Hamilton 路径** 2 | 3 | 状态压缩 + 动态规划 典型例题 4 | 5 | https://www.acwing.com/problem/content/93/ 6 | 7 | 8 | 9 | 给定一张 $n$ 个点的带权无向图,点从 $0∼n−1$ 标号,求起点 $0$ 到终点 $n−1$ 的最短 Hamilton 路径。 10 | 11 | Hamilton 路径的定义是从 $0$ 到 $n−1$ 不重不漏地经过每个点恰好一次。 12 | 13 | ### 输入格式 14 | 15 | 第一行输入整数 $n$。 16 | 17 | 接下来 $n$ 行每行 $n$ 个整数,其中第 $i$ 行第 $j$ 个整数表示点 $i$ 到 $j$ 的距离(记为 $a[i,j]$)。 18 | 19 | 对于任意的 $x,y,z$,数据保证 $a[x,x]=0$,$a[x,y]=a[y,x]$ 并且 $a[x,y]+a[y,z]≥a[x,z]$。 20 | 21 | ### 输出格式 22 | 23 | 输出一个整数,表示最短 Hamilton 路径的长度。 24 | 25 | ### 数据范围 26 | 27 | $1≤n≤20$ 28 | $0≤a[i,j]≤10^7$ 29 | 30 | ### 输入样例 31 | 32 | > 5 33 | > 0 2 4 5 1 34 | > 2 0 6 5 3 35 | > 4 6 0 8 3 36 | > 5 5 8 0 5 37 | > 1 3 3 5 0 38 | 39 | ### 输出样例 40 | 41 | > 18 42 | 43 | ### 题解 44 | 45 | #### 状态表示 46 | 47 | $dp(i,j):=$ 从节点 $0$ 走到节点 $j$,经过的节点状态为 $i$ 的所有路径的距离的**最小值**。 48 | 49 | $dis(i,j):=$ 从节点 $i$ 走到节点 $j$ 的距离。 50 | 51 | 对于经过的节点状态为 $i$,我们采用状态压缩的方式来表示它:$i$ 为一个 $20$ 位二进制数,其第 $n$ 位的状态代表是否走过第 $n$ 个点。 52 | 53 | 举例来说,若 $i=0100,0000,0000,0000,0011_b$,则代表经过了第 $0,1,18$ 个点,其他点没有经过。 54 | 55 | #### 状态转移 56 | 57 | 令节点 $j$ 的前一个节点为 $k$,即路径为:$0\rightarrow k\rightarrow j$ 58 | 59 | 那么可知:$dp(i,j)=dp(i-\{j\},k)+dis(k,j)$ 60 | 61 | 由于 $dp(i,j)$ 储存的是最小值,因此对于 $k=0,1,\dots,n-1$,取所有答案的最小值即可。 62 | 63 | #### 初值 64 | 65 | $dp(1,0)=0$:从 $0$ 走到 $0$,距离为 $0$,经过了第 $0$ 个点,因此 $i=1,j=0,dp(i,j)=0$ 66 | 67 | ### 代码 68 | 69 | ```cpp 70 | #include 71 | 72 | using namespace std; 73 | 74 | const int MAXN = 16; 75 | int n; 76 | int a[MAXN][MAXN]; 77 | int dp[1 << MAXN][MAXN]; 78 | 79 | int main() 80 | { 81 | cin >> n; 82 | for (int i = 0; i < n; i++) 83 | for (int j = 0; j < n; j++) 84 | cin >> a[i][j]; 85 | memset(dp, 0x3f, sizeof(dp)); 86 | dp[1][0] = 0; 87 | for (int i = 0; i < 1 << n; i++) 88 | for (int j = 0; j < n; j++) 89 | if (i >> j & 1) 90 | for (int k = 0; k < n; k++) 91 | if ((i - (1 << j)) >> k & 1) 92 | dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + a[k][j]); 93 | cout << dp[(1 << n) - 1][n - 1] << endl; 94 | return 0; 95 | } 96 | ``` 97 | 98 | #### 重点解析 99 | 100 | 从上面的第 $18$ 行开始 101 | 102 | | 代码 | 解析 | 103 | | ------------------------------------------------------------ | ----------------------------------------------------------- | 104 | | for (int i = 0; i < 1 << n; i++) | 枚举所有的 $i$ 情况 | 105 | | for (int j = 0; j < n; j++) | 枚举所有的 $j$ 情况 | 106 | | if (i >> j & 1) | 筛去不合法情况(从 $0$ 走到 $j$,$i$ 中一定包含 $j$) | 107 | | for (int k = 0; k < n; k++) | 枚举所有的 $k$ 情况 | 108 | | if ((i - (1 << j)) >> k & 1) | 筛去不合法情况(从 $0$ 走到 $k$,$i-\{j\}$ 中一定包含 $k$) | 109 | | dp\[i\]\[j\] = min(dp\[i\]\[j\], dp\[i - (1 << j)\]\[k\] + a\[k\]\[j\]); | 状态转移,取最小值 | 110 | 111 | -------------------------------------------------------------------------------- /070~079/079【题目】序列统计.md: -------------------------------------------------------------------------------- 1 | **LibreOJ10235.** 序列统计 2 | 3 | https://loj.ac/p/10235 4 | 5 | 6 | 7 | ### 题目描述 8 | 9 | 给定三个正整数 $N,L$ 和 $R$,统计长度在 $1$ 到 $N$ 之间,元素大小都在 $L$ 到 $R$ 之间的单调不降序列的数量。输出答案对 $10^6+3$ 取模的结果。 10 | 11 | ### 输入格式 12 | 13 | 输入第一行包含一个整数 $T$,表示数据组数。 14 | 15 | 第二到第 $T+1$ 行每行包含三个整数 $N,L$ 和 $R$,$N,L$ 和 $R$ 的意义如题所述。 16 | 17 | ### 输出格式 18 | 19 | 输出包含 $T$ 行,每行有一个数字,表示你所求出的答案对 $10^6+3$ 取模的结果。 20 | 21 | ### 样例 22 | 23 | 输入 24 | 25 | > 2 26 | > 1 4 5 27 | > 2 4 5 28 | 29 | 输出 30 | 31 | >2 32 | >5 33 | 34 | 对于第一组输入,满足条件的两个序列为 ${4},{5}$。 35 | 36 | ### 数据范围与提示 37 | 38 | 对于全部输入,$1\le N,L,R\le 10^9,1\le T\le 100$,输入数据保证 $L\le R$。 39 | 40 | ### 题解 41 | 42 | **长度为 $N$,令 $L=1$ 时** 43 | 44 | 我们先从长度为 $N$ 的单调不减序列 $a_1,a_2,\dots,a_N$ 入手,其中 $a_i\in[1,R]$. 45 | 46 | 单调不减不好处理,我们令 $b_i=a_i+(i-1)$,即可得到一个单调增序列 $b_1,b_2,\dots,b_N$,其中 $b_i\in[1,R+(N-1)]$. 47 | 48 | 原序列与变换后的序列能够保证一一对应,并且若变换后的序列满足单调增,原序列一定单调不减。 49 | 50 | 借助新序列,我们容易得出这种情况的数量为:$C_{R+N-1}^{N}$ 51 | 52 | **长度为 $N$ 时** 53 | 54 | 我们将上面的结论推广,计算长度为 $N$ 的单调不减序列 $a_1,a_2,\dots,a_N$ 的情况数,其中 $a_i\in[L,R]$. 55 | 56 | 我们令 $c_i=a_i-L+1$,即可得到一个新的单调不减序列 $c_1,c_2,\dots,c_N$,其中 $c_i\in[1,R-L+1]$,我们就可以再套用上面的结论。 57 | 58 | 这种情况的数量为:$C_{R-L+N}^{N}$ 59 | 60 | **长度为 $1,2,\dots,N$ 的总数** 61 | 62 | 最后我们需要将所有情况加到一起,即: 63 | $$ 64 | \sum_{n=1}^{N}C_{R-L+n}^n 65 | $$ 66 | 直接求和肯定要超时,尝试进行变形: 67 | $$ 68 | \begin{align} 69 | &\sum_{n=1}^{N}C_{R-L+n}^n\\ 70 | =&\sum_{n=1}^{N}C_{R-L+n}^{R-L}\\ 71 | =&\sum_{n=1}^{N}C_{R-L+n}^{R-L}+C_{R-L+1}^{R-L+1}-1\\ 72 | =&\sum_{n=2}^{N}C_{R-L+n}^{R-L}+C_{R-L+1}^{R-L}+C_{R-L+1}^{R-L+1}-1\\ 73 | =&\sum_{n=2}^{N}C_{R-L+n}^{R-L}+C_{R-L+2}^{R-L+1}-1\\ 74 | =&\sum_{n=3}^{N}C_{R-L+n}^{R-L}+C_{R-L+2}^{R-L}+C_{R-L+2}^{R-L+1}-1\\ 75 | =&\sum_{n=3}^{N}C_{R-L+n}^{R-L}+C_{R-L+3}^{R-L+1}-1\\ 76 | &\vdots\\ 77 | =&C_{R-L+N+1}^{R-L+1}-1 78 | \end{align} 79 | $$ 80 | 最后结果仅为一个组合数。数据规模 $10^9$,对质数 $10^6+3$ 取模,因此考虑使用卢卡斯定理求解该组合数。 81 | 82 | ### 代码 83 | 84 | ```cpp 85 | #include 86 | 87 | using namespace std; 88 | 89 | const int MOD = 1e6 + 3; 90 | long long fact[MOD + 10]; 91 | 92 | void init() 93 | { 94 | fact[0] = 1; 95 | for (int i = 1; i <= MOD; i++) 96 | fact[i] = fact[i - 1] * i % MOD; 97 | } 98 | 99 | long long fast_pow(long long a, long long b, long long p) 100 | { 101 | b %= p; 102 | long long ans = 1; 103 | while (b) 104 | { 105 | if (b % 2) 106 | ans = a * ans % p; 107 | a = a * a % p; 108 | b /= 2; 109 | } 110 | return ans; 111 | } 112 | 113 | inline long long inv(long long x, long long p) 114 | { 115 | return fast_pow(x, p - 2, p); 116 | } 117 | 118 | long long comb(long long a, long long b, long long p) 119 | { 120 | if (b > a) 121 | return 0; 122 | if (a < p && b < p) 123 | return fact[a] * inv(fact[b], p) % p * inv(fact[a - b], p) % p; 124 | return comb(a % p, b % p, p) * comb(a / p, b / p, p) % p; 125 | } 126 | 127 | int main() 128 | { 129 | int n; 130 | cin >> n; 131 | init(); 132 | while (n--) 133 | { 134 | long long N, L, R; 135 | cin >> N >> L >> R; 136 | cout << ((comb(N + (R - L + 1), R - L + 1, MOD) - 1) % MOD + MOD) % MOD << endl; 137 | } 138 | return 0; 139 | } 140 | ``` 141 | 142 | -------------------------------------------------------------------------------- /150~159/157【题目】简单的加法乘法计算题.md: -------------------------------------------------------------------------------- 1 | **2023 年华中科技大学程序设计竞赛新生赛** 2 | 3 | A - 简单的加法乘法计算题 4 | 5 | https://www.luogu.com.cn/problem/P9769?contestId=138479 6 | 7 | 8 | 9 | ## 题目 10 | 11 | JokerShaco 有一个数字 $x$,最开始 $x=0$,他想要把 $x$ 变成 $y$。为了达到这个目标,他可以利用两个集合 $A$ 和 $B$。其中集合 $A$ 包含 $n$ 个元素,分别是从 $1$ 到 $n$ 的所有正整数;集合 $B$ 包含 $m$ 个元素。每次它可以对 $x$ 进行如下任意次操作: 12 | - 选择 $A$ 中的一个元素 $a$,令 $x$ 加上 $a$。 13 | - 选择 $B$ 中的一个元素 $b$,令 $x$ 乘以 $b$。 14 | 15 | 已知 $y$,$n$,$m$ 和 $B$ 中 $m$ 个元素的具体值,JokerShaco 想知道让 $x$ 变成 $y$ 的最少操作次数。 16 | 17 | ## 输入 18 | 19 | 第一行包含三个整数 $y\ (1\le y\le 5\cdot 10^6)$,$n\ (1\le n\le 5\cdot 10^6)$ 和 $m\ (1\le m\le 10)$,其含义如题目所述。 20 | 21 | 第二行包含 $m$ 个正整数,其中第 $i$ 个表示 $B$ 中的第 $i$ 个元素 $b_i\ (1\le b_i\le 5\cdot 10^6)$。 22 | 23 | ## 输出 24 | 25 | 输出一个整数,表示让 $x$ 变成 $y$ 的最少操作次数。在题目条件下可知一定能将 $x$ 变成 $y$。 26 | 27 | ## 样例 28 | 29 | 样例输入 #1 30 | 31 | > 10 3 1 32 | > 2 33 | 34 | 样例输出 #1 35 | 36 | > 3 37 | 38 | 样例输入 #2 39 | 40 | > 100 6 3 41 | > 2 3 5 42 | 43 | 样例输出 #2 44 | 45 | > 3 46 | 47 | ## 题解 48 | 49 | **动态规划** 50 | 51 | 朴素的动态规划思想很好理解: 52 | 53 | - 状态表示 54 | - $dp_i:=$ 从 $0$ 变化到 $i$ 需要的步数 55 | - 状态转移 56 | - $A$ 操作:$dp_i:=\min\{dp_i,\min_{j=1}^{n}dp_{i-j}+1\}$ 57 | - $B$ 操作:$dp_i:=\min\{dp_i,\min_{j=1}^{m}dp_{i/b_j}+1\}$(前提是 $i\bmod b_j = 0$) 58 | 59 | 代码片段如下: 60 | 61 | ```cpp 62 | for (int i = n + 1; i <= b; i++) 63 | { 64 | for (int j = 1; j <= n; j++) 65 | dp[i] = min(dp[i], dp[i - j] + 1); 66 | for (int j = 1; j <= m; j++) 67 | if (i % b[j] == 0) 68 | dp[i] = min(dp[i], dp[i / b[j]] + 1); 69 | } 70 | ``` 71 | 72 | 容易得到,时间复杂度为:$O(n^2+nm)$ 73 | 74 | 根据 $1\le n\le 5\cdot 10^6,\; 1\le m\le 10$,可知复杂度过高,并且瓶颈在 $A$ 操作的转换中,需要对其进行优化。 75 | 76 | **优化复杂度** 77 | 78 | 从上文可知,每次转移时,需要花费 $O(n)$ 的时间来取得 $\min_{j=1}^{n}dp_{i-j}+1$ 的值,即 $dp_{i-n}\sim dp_{i-1}$ 这样一个长度为 $n$ 的滑动窗口中的最小值。 79 | 80 | 对于滑动窗口最值问题,直接使用[单调队列](https://io.zouht.com/6.html)即可解决了。单调队列的复杂度为 $O(n)$,每次查询的复杂度为 $O(1)$,因此总复杂度为:$O(n)$ 81 | 82 | ## 代码 83 | 84 | ```cpp 85 | #include 86 | #define endl '\n' 87 | #define int long long 88 | 89 | using namespace std; 90 | 91 | constexpr int MAXN = 5e6 + 10; 92 | int dp[MAXN]; 93 | 94 | void solve() 95 | { 96 | memset(dp, 0x3f, sizeof(dp)); 97 | int y, n, m; 98 | cin >> y >> n >> m; 99 | vector b(m); 100 | for (int i = 0; i < m; i++) 101 | cin >> b[i]; 102 | deque dque; 103 | for (int i = 1; i <= n; i++) 104 | { 105 | dp[i] = 1; 106 | dque.push_back(i); 107 | } 108 | for (int i = n + 1; i <= y; i++) 109 | { 110 | while (dque.size() && dque.front() + n < i) 111 | dque.pop_front(); 112 | for (int j = 0; j < m; j++) 113 | if (i % b[j] == 0) 114 | dp[i] = min(dp[i], dp[i / b[j]] + 1); 115 | dp[i] = min(dp[i], dp[dque.front()] + 1); 116 | while (dque.size() && dp[dque.back()] > dp[i]) 117 | dque.pop_back(); 118 | dque.push_back(i); 119 | } 120 | cout << dp[y] << endl; 121 | } 122 | 123 | signed main() 124 | { 125 | ios::sync_with_stdio(false); 126 | cin.tie(0); 127 | cout.tie(0); 128 | int t = 1; 129 | // cin >> t; 130 | while (t--) 131 | solve(); 132 | return 0; 133 | } 134 | ``` 135 | 136 | -------------------------------------------------------------------------------- /010~019/012【题目】全体集合.md: -------------------------------------------------------------------------------- 1 | **牛客小白月赛43** 2 | 3 | F - 全体集合 4 | 5 | https://ac.nowcoder.com/acm/contest/11220/F 6 | 7 | 8 | 9 | 时间限制:C/C++ 2秒,其他语言4秒 10 | 空间限制:C/C++ 262144K,其他语言524288K 11 | 64bit IO Format: %lld 12 | 13 | ## 题目描述 14 | 15 | 给出 $n$ 个点 $m$ 条边 的无向图,给出 $k$ 个点,这 $k$ 个点上每个点都有一个人,每个人每回合能走到一个相邻的节点(不能停留不走),问:有没有可能在某一个回合,让这些人都集中在一个点? 16 | 17 | ## 输入描述: 18 | 19 | > 第一行包含三个整数 $n$ $m$ $k$ ,$1\le n,k\le 2e5$,$0\le m\le min(5e5,n*(n-1)/2)$。 20 | > 21 | > 接下来 $m$ 行,每行包括两个整数 $u$ $v$,表示存在一条从节点 $u$ 到 $v$ 的边,节点编号从 $1$ 开始,保证图联通,无重边,无自环。 22 | > 23 | > 接下来一行,包含 $k$ 个整数,分别表示有人所在的节点,保证 $k$ 个节点各不相同。 24 | 25 | ## 输出描述: 26 | 27 | > 输出包括一行,如果有办法能让全部人都集中在一个点,输出**”YES“**,否则输出**”NO“**,不包含引号。 28 | 29 | ## 示例1 30 | 31 | ### 输入 32 | 33 | > 3 2 2 34 | > 1 2 35 | > 2 3 36 | > 1 3 37 | 38 | ### 输出 39 | 40 | > YES 41 | 42 | ### 说明 43 | 44 | > 第一回合,让节点 $1$ 的人走到节点 $2$,节点 $3$ 的人走到节点 $2$ ,全都集中了,输出**YES**。 45 | 46 | ## 示例2 47 | 48 | ### 输入 49 | 50 | > 3 2 2 51 | > 1 2 52 | > 2 3 53 | > 1 2 54 | 55 | ### 输出 56 | 57 | > NO 58 | 59 | ### 说明 60 | 61 | > 由于每回合不能不移动,不存在一种走法让全部人集中。 62 | 63 | ## 我的笔记 64 | 65 | ### 分析 66 | 67 | **问题转化** 68 | 69 | 容易发现,如果每个人之间的路程长度为偶数,那么所有人一定可以集中到一起。距离近的人集中后,可以“左右横跳”来等剩下的人集中,因为每个人距离是偶数,左右横跳需要 2 步,所以可以保证所有人都集中到一起。如果有两个人之间的路程长度为奇数,那么他们将无法集中。因为“反复横跳”后,这两个人总是隔着一个距离,没法集中到一起。 70 | 71 | 所以题目问题就转化为:判断所有人两两之间是否都有偶数路径。还可进一步处理为:随意选择一个人,他和其他人是否都有偶数路径。 72 | 73 | 需要注意,两个人之间可以既存在奇数路程,也存在偶数路程,因为图可以连接成网,选取不同的路径,路程长度可以不同。 74 | 75 | **算法选择** 76 | 77 | 可以先随意选择一个人(源码中选择的是第一个人),然后用 BFS 将这个人与每个节点的路径种类(即,是否有偶数路径、技术路径)计算出来,最后遍历一遍有人的节点是否都有偶数路径即可。 78 | 79 | **注意点** 80 | 81 | 这一题 BFS 的子节点比较特殊,并不是直接为节点序号。因为我们既要计算是否有偶数路径,也要计算是否有奇数路径,这样才能用 BFS ,即:上一个有奇数路径,这一个就有偶数路径;上一个有偶数路径,这一个就有奇数路径。 82 | 83 | 因此子节点用了一个二元组表示,每个节点序号包含两个子节点,一个用来判断奇数路径,一个用来判断偶数路径。父子节点的递推就是,若父节点判断的是奇数(偶数)路径,子节点就是判断所有与父节点连接的点的偶数(奇数)路径情况。 84 | 85 | ### 源码 86 | 87 | ```cpp 88 | #include 89 | 90 | using namespace std; 91 | 92 | int n, m, k; 93 | vector connect[200010]; 94 | vector people; 95 | bool dis[200010][2]; 96 | queue que; 97 | 98 | void bfs(void) 99 | { 100 | queue> que; 101 | que.push({people[0], 0}); 102 | while (!que.empty()) 103 | { 104 | pair now = que.front(); 105 | que.pop(); 106 | for (int i = 0; i < connect[now.first].size(); i++) 107 | { 108 | pair next = {connect[now.first][i], now.second ? 0 : 1}; 109 | if (!dis[next.first][next.second]) 110 | { 111 | dis[next.first][next.second] = 1; 112 | que.push({next.first, next.second}); 113 | } 114 | } 115 | } 116 | } 117 | 118 | int main(void) 119 | { 120 | cin >> n >> m >> k; 121 | for (int i = 0; i < m; i++) 122 | { 123 | int u, v; 124 | scanf("%d%d", &u, &v); 125 | connect[u].push_back(v); 126 | connect[v].push_back(u); 127 | } 128 | for (int i = 0; i < k; i++) 129 | { 130 | int t; 131 | scanf("%d", &t); 132 | people.push_back(t); 133 | } 134 | bfs(); 135 | bool flag = true; 136 | for (int i = 0; i < people.size(); i++) 137 | { 138 | if (!dis[people[i]][0]) 139 | { 140 | flag = false; 141 | break; 142 | } 143 | } 144 | if (flag) 145 | { 146 | cout << "YES" << endl; 147 | } 148 | else 149 | { 150 | cout << "NO" << endl; 151 | } 152 | return 0; 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /020~029/029【算法】SPFA (最短路径快速算法).md: -------------------------------------------------------------------------------- 1 | **解决赋权图的单源最短路径问题:**SPFA (Shortest Path Faster Algorithm, 最短路径快速算法) - Bellman-Ford 的队列优化算法 2 | 3 | - 能解决负边 4 | - 能解决负环 5 | 6 | 7 | 8 | # SPFA 9 | 10 | ## 复杂度 11 | 12 | 平均时间复杂度:$O(\left|E\right|)$ 13 | 14 | 最差时间复杂度:$O(\left|V\right|\cdot\left|E\right|)$ 15 | 16 | ($\left|V\right|$ 为顶点数,$\left|E\right|$ 为边数) 17 | 18 | ## 分析 19 | 20 | SPFA 算法为 Bellman-Ford 算法的优化,所以建议先学习 Bellman-Ford 算法:https://io.zouht.com/64.html 21 | 22 | 在 Bellman-Ford 算法中,每一次迭代我们一股脑地对所有边进行了松弛操作,但实际上很多松弛操作是没有意义的。如果一个点距离没有更新,那么没有必要对它进行松弛操作,因为答案根本不会变化。SPFA 就是针对这一点进行了优化。 23 | 24 | SPFA 使用一个队列,若一个点被更新了,那么就将其加入队列,等待松弛操作。简单来说就是只有距离被更新过的点才进行松弛操作,若一个点没有变化,不会对其进行操作。 25 | 26 | ## 代码实现 27 | 28 | ```cpp 29 | const int MAXN = 1e5 + 10, INF = 0x3f3f3f3f; // INF代表无穷大 30 | int e, v, s; // e边数 v顶点数 s起点 31 | vector> edge[MAXN]; // 邻接表存图 pair为(距离, 终点) 32 | queue que; // STL队列 33 | int dist[MAXN]; // 最短距离 34 | bool vis[MAXN]; // 是否入队 35 | 36 | void spfa(int src) 37 | { 38 | memset(dist, 0x3f, sizeof(dist)); 39 | dist[src] = 0; 40 | que.push(src); 41 | vis[src] = true; 42 | while (!que.empty()) 43 | { 44 | int cur = que.front(); 45 | que.pop(); 46 | vis[cur] = false; 47 | for (auto next : edge[cur]) 48 | { 49 | if (dist[next.second] > dist[cur] + next.first) 50 | { 51 | dist[next.second] = dist[cur] + next.first; 52 | if (!vis[next.second]) 53 | { 54 | que.push(next.second); 55 | vis[next.second] = true; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ## 利与 SPFA 判断负权回路 64 | 65 | 由于 SPFA 为 Bellman-Ford 算法的优化,所以它也可以判断负权回路,思路就是记录一个点被松弛了多少次。若没有负环一个点最多被松弛 $\left|V\right|-1$ 次,但如果有负环,一个点可以被松弛 $\infty$ 次。只需要对代码稍作修改即可: 66 | 67 | - 无需将 dist 初始化为 $\infty$:因为我们只判负环,不在意距离是多少,因此无需初始化。 68 | - 算法开始时需要将所有点入队:若只以一个点为起点,可能有些负环不与其连通无法判断到,因此得每个点都判断一遍。 69 | - 增加一个 cnt 数组:储存被松弛多少次,一个点最多被松弛 $\left|V\right|-1$ 次,若大于它一定是出现了负环。 70 | 71 | 更改后的代码如下: 72 | 73 | ```cpp 74 | const int MAXN = 1e5 + 10, INF = 0x3f3f3f3f; 75 | int E, V, S; 76 | vector> edge[MAXN]; 77 | queue que; 78 | int dist[MAXN], cnt[MAXN]; 79 | bool vis[MAXN]; 80 | 81 | bool spfa() 82 | { 83 | for (int i = 1; i <= V; i++) 84 | { 85 | que.push(i); 86 | vis[i] = true; 87 | } 88 | while (!que.empty()) 89 | { 90 | int cur = que.front(); 91 | que.pop(); 92 | vis[cur] = false; 93 | for (auto next : edge[cur]) 94 | { 95 | if (dist[next.second] > dist[cur] + next.first) 96 | { 97 | dist[next.second] = dist[cur] + next.first; 98 | cnt[next.second] = cnt[cur] + 1; 99 | if (cnt[next.second] >= V) 100 | return true; 101 | if (!vis[next.second]) 102 | { 103 | que.push(next.second); 104 | vis[next.second] = true; 105 | } 106 | } 107 | } 108 | } 109 | return false; 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /110~119/114【题目】Matches.md: -------------------------------------------------------------------------------- 1 | **“范式杯”2023牛客暑期多校训练营1** 2 | 3 | H - Matches 4 | 5 | https://ac.nowcoder.com/acm/contest/57355/H 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 给定两个长度为 $n$ 的序列 $\{a\}_{i=1}^n$ 和 $\{b\}_{i=1}^n$, 现在可以选择其中一个序列交换其中的两个数字, 问经过至多一 次操作后最小的 $\sum_{i=1}^n\left|a_i-b_i\right|$。$1 \leq n \leq 2 \times 10^5, 0 \leq\left|a_i\right|,\left|b_i\right| \leq 10^{12}$ 。 12 | 13 | ## 题解 14 | 15 | 对于一对 $(a_i,b_i),(a_j,b_j)$,如果它们的大小关系相同,称为正序,若相反成为反序。另外,如果其中区间将另一个区间包含起来,称为包络,如果一个区间和另一个区间有交集,则称为相交。 16 | 17 | 18 | 19 | 如上图,对于一对 $(a_i,b_i),(a_j,b_j)$,交换后差值减小的情况只有**反序包络**与**反序相交**。因此考虑交换时,只需要考虑这两种情况。另外,差值减小的幅度 $\Delta$ 和两个区间相交的长度有关,具体来说,若两个反序区间相交的距离为 $\text{cross}$,那么 $\Delta=2\cdot\text{cross}$. 因此,在选择包络或相交的区间时,要选择相交长度最长的。 20 | 21 | 首先,我们将区间分成两类,$ab$ 的区间的集合为 $t$,另外 $a=b$ 的情况不会对最终答案产生影响,所以直接忽略。那我们的思路就变成,遍历其中一个集合的每一个区间,在另一个集合中找与其包络或相交的区间,下面代码遍历的 $t$. 22 | 23 | 那现在的问题就是,对于 $t$ 中每一个区间,如何快速的在 $s$ 中找到符合要求的最好的区间。 24 | 25 | 首先容易发现,如果 $s$ 中一个区间被另一个区间完全包含,那么我们选择大的区间,答案一定不会比小区间差。因为上文说到相交长度越长,减小幅度越大,$s$ 中大区间与当前 $t$ 中的区间的相交长度一定 $\geq$ $s$ 中被包含的小区间的相交长度。因此,应当将 $s$ 中的所有有包含关系的区间去除,保留最大区间。 26 | 27 | 经过上面的操作后,只要在 $s$ 中选择一个符合条件的区间,则必为对应条件的最优区间。由于 $s$ 中没有互相包含的区间,因此获得一个新性质,我们对 $s$ 的左端点进行排序后,$s$ 的右端点其实也有序了,这样的话左右端点都可以用二分找到了。 28 | 29 | 对于 $t$ 中的一个区间 $(x,y)$: 30 | 31 | - 找 $s$ 中与它相交的 32 | - 找左侧相交的:在 $s$ 中区间二分找左端点 $\leq x$ 的 33 | - 找右侧相交的:在 $s$ 中区间二分找右端点 $\geq y$ 的 34 | - 找 $s$ 中与它包络的 35 | - 在 $s$ 中区间二分找左端点 $\leq x$ 的索引 $l$,找右端点 $\geq y$ 的索引 $r$,那么 $(l,r)$ 的区间就是被 $t$ 包络。 36 | 37 | ## 代码 38 | 39 | ```cpp 40 | #include 41 | #define endl '\n' 42 | #define int long long 43 | 44 | using namespace std; 45 | 46 | void solve() 47 | { 48 | int n; 49 | cin >> n; 50 | vector> a(n); 51 | int sum = 0, cross = 0; 52 | for (auto &p : a) 53 | cin >> p.first; 54 | for (auto &p : a) 55 | cin >> p.second; 56 | vector> s, t; 57 | for (auto &[x, y] : a) 58 | { 59 | sum += abs(x - y); 60 | if (x < y) 61 | s.emplace_back(x, y); 62 | else if (x > y) 63 | t.emplace_back(y, x); 64 | } 65 | sort(s.begin(), s.end()); 66 | vector sx, sy, len; 67 | int rpos = INT64_MIN; 68 | for (auto &[x, y] : s) 69 | { 70 | if (y <= rpos) 71 | continue; 72 | rpos = y; 73 | sx.push_back(x); 74 | sy.push_back(y); 75 | len.push_back(y - x); 76 | } 77 | for (auto &[x, y] : t) 78 | { 79 | int tmp = 0; 80 | int lpos = upper_bound(sx.begin(), sx.end(), x) - sx.begin(); 81 | int rpos = lower_bound(sy.begin(), sy.end(), y) - sy.begin(); 82 | if (lpos > 0) 83 | tmp = max(tmp, min(y - x, sy[lpos - 1] - x)); 84 | if (rpos < sy.size()) 85 | tmp = max(tmp, min(y - x, y - sx[rpos])); 86 | if (rpos > lpos) 87 | tmp = max(tmp, *max_element(len.begin() + lpos, len.begin() + rpos)); 88 | cross = max(cross, tmp); 89 | } 90 | cout << sum - cross * 2 << endl; 91 | } 92 | 93 | signed main() 94 | { 95 | ios::sync_with_stdio(false); 96 | cin.tie(0); 97 | cout.tie(0); 98 | solve(); 99 | return 0; 100 | } 101 | ``` 102 | 103 | -------------------------------------------------------------------------------- /020~029/023【题目】Range Sums.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 238** 2 | 3 | E - Range Sums 4 | 5 | https://atcoder.jp/contests/abc238/tasks/abc238_e 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | Takahashi has a secret integer sequence $a$. You know that the length of $a$ is $N$. 16 | 17 | You want to guess the contents of $a$. He has promised to give you the following $Q$ additional pieces of information. 18 | 19 | - The $i$-th information: the value $a_{l_i}+a_{l_i+1}+\cdots+a_{r_i}$. 20 | 21 | Is it possible to determine the sum of all elements in $a$, $a_1+a_2+\cdots+a_N$, if the $Q$ pieces of promised information are given? 22 | 23 | ### Constraints 24 | 25 | - $1 \leq N \leq 2 \times 10^5$ 26 | - $1 \leq Q \leq \min(2 \times 10^5,\frac{N(N+1)}{2})$ 27 | - $1 \leq l_i \leq r_i \leq N$ 28 | - $(l_i,r_i) \neq (l_j,r_j)\ (i \neq j)$ 29 | - All values in input are integers. 30 | 31 | ------ 32 | 33 | ### Input 34 | 35 | Input is given from Standard Input in the following format: 36 | 37 | > $N$ $Q$ 38 | > $l_1$ $r_1$ 39 | > $l_2$ $r_2$ 40 | > $\hspace{0.4cm}\vdots$ 41 | > $l_Q$ $r_Q$ 42 | 43 | ### Output 44 | 45 | If it is possible to determine the sum of all elements in $a$, print `Yes`; otherwise, print `No`. 46 | 47 | ------ 48 | 49 | ### Sample Input 1 50 | 51 | > 3 3 52 | > 1 2 53 | > 2 3 54 | > 2 2 55 | 56 | ### Sample Output 1 57 | 58 | > Yes 59 | 60 | From the first and second information, we can find the value $a_1+a_2+a_2+a_3$. By subtracting the value of $a_2$ from it, we can determine the value $a_1+a_2+a_3$. 61 | 62 | ------ 63 | 64 | ### Sample Input 2 65 | 66 | > 4 3 67 | > 1 3 68 | > 1 2 69 | > 2 3 70 | 71 | ### Sample Output 2 72 | 73 | > No 74 | 75 | We can determine the sum of the first $3$ elements of $a$, but not the sum of all elements. 76 | 77 | ------ 78 | 79 | ### Sample Input 3 80 | 81 | > 4 4 82 | > 1 1 83 | > 2 2 84 | > 3 3 85 | > 1 4 86 | 87 | ### Sample Output 3 88 | 89 | > Yes 90 | 91 | The fourth information directly gives us the sum of all elements. 92 | 93 | ### 我的笔记 94 | 95 | 如果能想到并查集这题非常的简单。 96 | 97 | 首先有 $N$ 个数,我们把这些数排成一排,那么就有 $N+1$ 个间隔(包括两端),然后我们把每一个 $l$ $r$ 对应到这 $N+1$ 个间隔,只要最后 $0\sim N+1$ 相连,那么就说明可以求出和。 98 | 99 | 如下图,表示的就是 1 6;2 4;2 6;5 8 这四个联通,最后 0 和 8 联通,即可以求出和。 100 | 101 | 102 | 103 | 求联通,很简单想到用并查集,因此初始化一个大小为 $N+1$ 的并查集即可。 104 | 105 | 另外,这题必须要用路径优化,否则会有两个测试点 TLE,按秩合并倒不重要,实测按秩合并效率反而略低。 106 | 107 | ### 源码 108 | 109 | ```cpp 110 | #include 111 | #define MAXN 200010 112 | 113 | using namespace std; 114 | 115 | int fa[MAXN]; 116 | inline void init(int n) 117 | { 118 | for (int i = 0; i < n; i++) 119 | fa[i] = i; 120 | } 121 | 122 | int find(int x) 123 | { 124 | return x == fa[x] ? x : (fa[x] = find(fa[x])); 125 | } 126 | 127 | inline void merge(int i, int j) 128 | { 129 | fa[find(i)] = find(j); 130 | } 131 | 132 | int main(void) 133 | { 134 | ios::sync_with_stdio(false); 135 | int N, Q; 136 | cin >> N >> Q; 137 | init(N + 1); 138 | while (Q--) 139 | { 140 | int l, r; 141 | cin >> l >> r; 142 | merge(l - 1, r); 143 | } 144 | if (find(0) == find(N)) 145 | { 146 | cout << "Yes" << endl; 147 | } 148 | else 149 | { 150 | cout << "No" << endl; 151 | } 152 | return 0; 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /040~049/041【题目】2-variable Function.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 246** 2 | 3 | D - 2-variable Function 4 | 5 | https://atcoder.jp/contests/abc246/tasks/abc246_d 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $400$ points 12 | 13 | ### Problem Statement 14 | 15 | Given an integer $N$, find the smallest integer $X$ that satisfies all of the conditions below. 16 | 17 | - $X$ is greater than or equal to $N$. 18 | - There is a pair of non-negative integers $(a, b)$ such that $X=a^3+a^2b+ab^2+b^3$. 19 | 20 | ### Constraints 21 | 22 | - $N$ is an integer. 23 | - $0 \le N \le 10^{18}$ 24 | 25 | ------ 26 | 27 | ### Input 28 | 29 | Input is given from Standard Input in the following format: 30 | 31 | > $N$ 32 | 33 | ### Output 34 | 35 | Print the answer as an integer. 36 | 37 | ------ 38 | 39 | ### Sample Input 1 40 | 41 | > 9 42 | 43 | ### Sample Output 1 44 | 45 | > 15 46 | 47 | For any integer $X$ such that $9 \le X \le 14$, there is no $(a, b)$ that satisfies the condition in the statement. 48 | For $X=15$, $(a,b)=(2,1)$ satisfies the condition. 49 | 50 | ------ 51 | 52 | ### Sample Input 2 53 | 54 | > 0 55 | 56 | ### Sample Output 2 57 | 58 | > 0 59 | 60 | $N$ itself may satisfy the condition. 61 | 62 | ------ 63 | 64 | ### Sample Input 3 65 | 66 | > 999999999989449206 67 | 68 | ### Sample Output 3 69 | 70 | > 1000000000000000000 71 | 72 | Input and output may not fit into a $32$-bit integer type. 73 | 74 | ### 我的笔记 75 | 76 | **直来直去思路:** 77 | 78 | $N_{max}=10^{18}$,$X=a^3+a^2b+ab^2+b^3\leq N_{max}$,因此 $\max(a,b)\leq 10^6$ 79 | 80 | 那么直来直去的想法就是 $a$ 从 $0$ 遍历到 $10^6$,对于每个 $a$ 用二分法找到 $b$,时间复杂度 $O(N\log N)$,可以通过。 81 | 82 | ```cpp 83 | #include 84 | 85 | using namespace std; 86 | 87 | inline long long f(long long a, long long b) 88 | { 89 | return a * a * a + a * a * b + a * b * b + b * b * b; 90 | } 91 | 92 | int main(void) 93 | { 94 | long long N; 95 | cin >> N; 96 | long long ans = INT64_MAX; 97 | long long delta = INT64_MAX; 98 | for (long long i = 0; i <= 1e6; i++) 99 | { 100 | long long l = i, r = 1e6; 101 | while (l < r) 102 | { 103 | long long mid = (l + r) >> 1; 104 | if (f(i, mid) >= N) 105 | r = mid; 106 | else 107 | l = mid + 1; 108 | } 109 | if (f(i, l) - N < delta) 110 | { 111 | delta = f(i, l) - N; 112 | ans = f(i, l); 113 | } 114 | } 115 | cout << ans << endl; 116 | return 0; 117 | } 118 | ``` 119 | 120 | **也挺简单的另一种思路(官解思路):** 121 | 122 | $a$ 从 $0$ 遍历到 $10^6$,对于每个 $a$,$b$ 从上一次的大小向 $0$ 遍历,直到 $X 127 | 128 | using namespace std; 129 | 130 | long long f(long long a, long long b) 131 | { 132 | return (a * a * a + a * a * b + a * b * b + b * b * b); 133 | } 134 | 135 | int main() 136 | { 137 | long long n; 138 | cin >> n; 139 | long long x = 8e18; 140 | long long j = 1000000; 141 | for (long long i = 0; i <= 1000000; i++) 142 | { 143 | while (f(i, j) >= n && j >= 0) 144 | { 145 | x = min(x, f(i, j)); 146 | j--; 147 | } 148 | } 149 | cout << x << '\n'; 150 | return 0; 151 | } 152 | ``` -------------------------------------------------------------------------------- /050~059/052【题目】Distance Sequence.md: -------------------------------------------------------------------------------- 1 | **NOMURA Programming Contest 2022(AtCoder Beginner Contest 253)** 2 | 3 | E - Distance Sequence 4 | 5 | https://atcoder.jp/contests/abc253/tasks/abc253_e 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | How many integer sequences $A=(A_1,\ldots,A_N)$ of length $N$ satisfy all the conditions below? 16 | 17 | - $1\le A_i \le M$ $(1 \le i \le N)$ 18 | - $|A_i - A_{i+1}| \geq K$ $(1 \le i \le N - 1)$ 19 | 20 | Since the count can be enormous, find it modulo $998244353$. 21 | 22 | ### Constraints 23 | 24 | - $2 \leq N \leq 1000$ 25 | - $1 \leq M \leq 5000$ 26 | - $0 \leq K \leq M-1$ 27 | - All values in input are integers. 28 | 29 | ------ 30 | 31 | ### Input 32 | 33 | Input is given from Standard Input in the following format: 34 | 35 | > $N$ $M$ $K$ 36 | 37 | ### Output 38 | 39 | Print the count modulo $998244353$. 40 | 41 | ------ 42 | 43 | ### Sample Input 1 44 | 45 | > 2 3 1 46 | 47 | ### Sample Output 1 48 | 49 | > 6 50 | 51 | The following $6$ sequences satisfy the conditions. 52 | 53 | - $(1,2)$ 54 | - $(1,3)$ 55 | - $(2,1)$ 56 | - $(2,3)$ 57 | - $(3,1)$ 58 | - $(3,2)$ 59 | 60 | ------ 61 | 62 | ### Sample Input 2 63 | 64 | > 3 3 2 65 | 66 | ### Sample Output 2 67 | 68 | > 2 69 | 70 | The following $2$ sequences satisfy the conditions. 71 | 72 | - $(1,3,1)$ 73 | - $(3,1,3)$ 74 | 75 | ------ 76 | 77 | ### Sample Input 3 78 | 79 | > 100 1000 500 80 | 81 | ### Sample Output 3 82 | 83 | > 657064711 84 | 85 | Print the count modulo $998244353$. 86 | 87 | ------ 88 | 89 | ### 我的笔记 90 | 91 | 很简单的一道 DP,令 $dp[i][j]:=$ 只考虑前 $i$ 个数,且第 $i$ 个数大小为 $j$ 的情况数量。转移方程: 92 | $$ 93 | dp[i+1][j]=(dp[i][1]+\dots+dp[i][j-K])+(dp[i][j+K]+\dots+dp[i][M]) 94 | $$ 95 | 暴力法时间复杂度为:$O(N\cdot M^2)$,可以用前缀和简单优化为 $O(N\cdot M)$ 96 | 97 | 导致我 WA 半天写不出来的时本题比较隐蔽的坑: $K=0$ 时的特判。 98 | 99 | 若 $K=0$,$dp[i][j-K]=dp[i][j+K]$,导致这一项重复了,解决方法就是直接特判,这种情况等于 $dp[i][1]+\dots+dp[i][M]$ 100 | 101 | ### 代码 102 | 103 | ```cpp 104 | #include 105 | 106 | using namespace std; 107 | 108 | const int MAXN = 1010, MAXM = 5010, MOD = 998244353; 109 | int N, M, K; 110 | long long dp[MAXN][MAXM]; 111 | long long ps[MAXM]; 112 | 113 | int main() 114 | { 115 | cin >> N >> M >> K; 116 | for (int i = 1; i <= M; i++) 117 | dp[1][i] = 1; 118 | for (int i = 2; i <= N; i++) 119 | { 120 | memset(ps, 0, sizeof(ps)); 121 | for (int j = 1; j <= M; j++) 122 | { 123 | ps[j] = ps[j - 1] + dp[i - 1][j]; 124 | ps[j] %= MOD; 125 | } 126 | for (int j = 1; j <= M; j++) 127 | { 128 | if (K == 0) 129 | { 130 | dp[i][j] = ps[M]; 131 | continue; 132 | } 133 | if (j - K >= 1) 134 | { 135 | dp[i][j] += MOD + ps[j - K] - ps[0]; 136 | dp[i][j] %= MOD; 137 | } 138 | if (j + K <= M) 139 | { 140 | dp[i][j] += MOD + ps[M] - ps[j + K - 1]; 141 | dp[i][j] %= MOD; 142 | } 143 | } 144 | } 145 | long long ans = 0; 146 | for (int i = 1; i <= M; i++) 147 | { 148 | ans += dp[N][i]; 149 | ans %= MOD; 150 | } 151 | cout << ans << endl; 152 | return 0; 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /100~109/103【算法】最长上升子序列.md: -------------------------------------------------------------------------------- 1 | **最长上升子序列 (LIS, Longest Increasing Subsequence):**在给定序列中找到最长的子序列,满足子序列升序。 2 | 3 | 4 | 5 | # 模板题:[Acwing 895](https://www.acwing.com/problem/content/897/) 6 | 7 | 给定一个长度为 $N$ 的数列,求数值严格单调递增的子序列的长度最长是多少。 8 | 9 | ## 输入格式 10 | 11 | 第一行包含整数 $N$。 12 | 13 | 第二行包含 $N$ 个整数,表示完整序列。 14 | 15 | ## 输出格式 16 | 17 | 输出一个整数,表示最大长度。 18 | 19 | ## 数据范围 20 | 21 | $1\leq N\leq 1000$ 22 | $−10^9\leq 数列中的数\leq 10^9$ 23 | 24 | ## 输入样例: 25 | 26 | > 7 27 | > 3 1 2 1 8 5 6 28 | 29 | ## 输出样例: 30 | 31 | > 4 32 | 33 | # $O(n^2)$ 解法:动态规划 34 | 35 | ## 流程和思路 36 | 37 | $a_i:=$ 序列中的第 $i$ 个数(下标从 $1$ 开始) 38 | 39 | $dp(i):=$ 以 $a_i$ 为结尾的子序列的最大长度 40 | 41 | 由于我们要求子序列严格单调上升,那么如果 $j 49 | 50 | using namespace std; 51 | 52 | const int MAXN = 1010; 53 | int N, a[MAXN]; 54 | int dp[MAXN]; 55 | 56 | int main() 57 | { 58 | a[0] = INT32_MIN; 59 | cin >> N; 60 | for (int i = 1; i <= N; i++) 61 | cin >> a[i]; 62 | for (int i = 1; i <= N; i++) 63 | for (int j = 0; j < i; j++) 64 | if (a[j] < a[i]) 65 | dp[i] = max(dp[i], dp[j] + 1); 66 | int ans = 0; 67 | for (int i = 1; i <= N; i++) 68 | ans = max(ans, dp[i]); 69 | cout << ans << endl; 70 | return 0; 71 | } 72 | ``` 73 | 74 | # $O(n\log n)$ 解法:贪心、二分、单调栈 75 | 76 | 单调栈:https://io.zouht.com/6.html 77 | 78 | ## 流程 79 | 80 | 使用一个数组 $v$ 作为单调递增栈,遍历 $a_1\sim a_n$,对于每个 $a_i$: 81 | 82 | - 若 $a_i>v_{top}$,则将 $a_i$ 入栈。 83 | - 若 $a_i\leq v_{top}$,则用 $a_i$ 替换掉 $v$ 中第一个 $\geq a_i$ 的数。 84 | 85 | 遍历完成整个序列后,$v$ 的长度即为最长上升子序列的长度。 86 | 87 | ## 思路 88 | 89 | 这种方法有一点理解难度,先用一个例子进行模拟: 90 | 91 | $a=[3,1,4,1,5,9,2]$ 92 | 93 | 循环过程中 $v$ 的变化: 94 | 95 | 1. $v=[3]$ 96 | 2. $v=[1]$ 97 | 3. $v=[1,4]$ 98 | 4. $v=[1,4]$ 99 | 5. $v=[1,4,5]$ 100 | 6. $v=[1,4,5,9]$ 101 | 7. $v=[1,2,5,9]$ 102 | 103 | 最终,$v$ 的长度为 $4$ 即最长上升子序列长度。 104 | 105 | 我们会发现一些奇怪之处: 106 | 107 | - 这个单调栈的去尾操作并不典型,$6\sim7$ 步时,数字 $9$ 没有被去除而是保留下来了。 108 | - 单调栈的序列并不是最长上升子序列,该例应该为 $[1,4,5,9]$ 而不是 $[1,2,5,9]$ 109 | 110 | ----- 111 | 112 | 首先解决第一个问题,为什么用这种特殊的去尾操作? 113 | 114 | 原因是贪心,我们要求的是最长的上升子序列,当然不愿意越算子序列长度反而越小了。因此,如果我们目前算出的结果还没以前的长,会暂时保留以前的结果,当然也不丢弃目前的结果,因为之后继续计算的话,目前的结果可能更优。 115 | 116 | 为了实现上述目的,我们可以用新序列从左到右逐渐覆盖掉旧序列。当新序列长度 $<$ 原序列长度时,原序列没有被完全覆盖,因此保证长度不减小;当新序列长度 $\geq$ 原序列长度时,原序列已经被完全覆盖,现在就是以新序列为基础进行计算了。 117 | 118 | 因此就产生了这种特别的去尾方式。上述例子的第 $7$ 步可以表示成: 119 | $$ 120 | \begin{align} 121 | 之前的更好的序列:v=&[1,4,5,9]\\ 122 | 现在的新序列:v=&[1,2]\\ 123 | 覆盖储存:v=&[1,2,5,9] 124 | \end{align} 125 | $$ 126 | 127 | ----- 128 | 129 | 第一个问题解决后,第二个问题其实就比较容易了,因为栈中储存的不只有一个序列,是以前的序列和现在的序列合并的产物,因此不一定是最终最长上升子序列。 130 | 131 | 但是对于长度,由于我们的贪心思想,我们循环过程中栈的长度不减,因此能够保证最长上升子序列的长度就是栈的长度。 132 | 133 | ## 代码 134 | 135 | ```cpp 136 | #include 137 | 138 | using namespace std; 139 | 140 | const int MAXN = 1e5 + 10; 141 | int N, a[MAXN]; 142 | vector v; 143 | 144 | int main() 145 | { 146 | cin >> N; 147 | for (int i = 1; i <= N; i++) 148 | cin >> a[i]; 149 | v.push_back(a[1]); 150 | for (int i = 2; i <= N; i++) 151 | { 152 | if (a[i] > v.back()) 153 | v.push_back(a[i]); 154 | else 155 | v[lower_bound(v.begin(), v.end(), a[i]) - v.begin()] = a[i]; 156 | } 157 | cout << v.size() << endl; 158 | return 0; 159 | } 160 | ``` 161 | 162 | -------------------------------------------------------------------------------- /120~129/126【题目】Qu'est-ce Que C'est.md: -------------------------------------------------------------------------------- 1 | **2023牛客暑期多校训练营4** 2 | 3 | J - Qu'est-ce Que C'est? 4 | 5 | https://ac.nowcoder.com/acm/contest/57358/J 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 给定长度为 $n$ 的数列 $\{a\}_{i=1}^n$, 要求每个数都在 $[-m, m]$ 范围, 且任意长度大于等于 2 的区间和都大于等于 0 问方案数。 $1 \leq n, m \leq 5 \times 10^3$ 。 12 | 13 | ## 题解 14 | 15 | 首先,要注意到如果现在数列已经填了 $x$ 个数 $a_1,a_2,\dots,a_x$,记此时从 $x$ 位开始的后缀和最小值为: 16 | $$ 17 | t=\min\left\{\sum_{i=1}^{x}a_i,\sum_{i=2}^{x}a_i,\dots,\sum_{i=x}^{x}a_i\right\} 18 | $$ 19 | 那么,在填下一个数 $a_{x+1}$ 时,$a_{x+1}$ 要满足: 20 | $$ 21 | -t\leq a_{x+1}\leq m 22 | $$ 23 | 下面简单解释一下为什么。 24 | 25 | 首先是为什么只关心后缀和。因为对于一个长为 $x$ 的数列,往后再加 $1$ 位,此时多出的子区间的和其实就是 $x$ 个后缀和。所以如果能保证 $x$ 长度的数列合法,那么往后再加 $1$ 位,如果多出的后缀和合法,则 $x+1$ 长度的数列也合法。 26 | 27 | 然后是为什么只关心最小值。因为如果保证最小的后缀和区间 $\geq0$,那其他区间就一定都 $\geq0$ 了。 28 | 29 | 最后一点为什么限制是这样。因为当 $t<0$ 时,为了保证后缀和 $\geq0$,那么必须填一个 $\geq -t$ 的数把后缀和的大小抬到 $0$ 以上,要不然就不成立了。而当 $t\geq0$ 时,不能填 $<-t$ 的数,因为这样会把一个后缀和压到 $0$ 以下。 30 | 31 | 现在,动态规划的思路大概就有了,首先是状态表示: 32 | 33 | - $dp_{i,j}:=$ 数列已经确定了 $i$ 位,且后缀和最小值为 $j$ 的情况下的方案总数。 34 | 35 | 接下来就是转移方式: 36 | 37 | - 如果 $j\geq0$:$dp_{i,j}$ 转移到 $dp_{i,a_{x+1}}$ 38 | - 如果 $j<0$:$dp_{i,j}$ 转移到 $dp_{i,j+a_{x+1}}$ 39 | 40 | 转移时跟 $j$ 大小有关的原因是,如果 $j\geq0$,那么新来一个 $a_{x+1}$,它这一位肯定是最小的,即: 41 | $$ 42 | a_{x+1}=\min\left\{a_{x+1}+\sum_{i=1}^{x}a_i,a_{x+1}+\sum_{i=2}^{x}a_i,\dots,a_{x+1}+\sum_{i=x}^{x}a_i,a_{x+1}\right\} 43 | $$ 44 | 而如果 $j<0$,那么最小的仍然是原来的那一位,只不过它的大小会被加上 $a_{x+1}$,即: 45 | $$ 46 | t+a_{x+1}=\min\left\{a_{x+1}+\sum_{i=1}^{x}a_i,a_{x+1}+\sum_{i=2}^{x}a_i,\dots,a_{x+1}+\sum_{i=x}^{x}a_i,a_{x+1}\right\} 47 | $$ 48 | 根据上述分析,就可以写出一个朴素的三重循环 DP 了: 49 | 50 | ```cpp 51 | for (int i = m; i >= -m; i--) 52 | dp[1][i + m] = 1; 53 | for (int i = 2; i <= n; i++) { 54 | for (int j = m; j >= -m; j--) { 55 | if (j >= 0) { 56 | for (int k = m; k >= j - m; k--) { 57 | dp[i][j + m] = (dp[i][j + m] + dp[i - 1][k + m]) % MOD; 58 | } 59 | } else { 60 | for (int k = m; k >= -j; k--) { 61 | dp[i][j + m] = (dp[i][j + m] + dp[i - 1][k + m]) % MOD; 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | 只不过这样时间复杂度为 $O(n^3)$,无法通过。不过能发现,第三重循环加的区间都是连续的,因此可以用前缀和优化,把 $O(n)$ 复杂度的区间和优化成 $O(1)$,这样时间复杂度优化为 $O(n^2)$ 即可通过了。 69 | 70 | ## 代码 71 | 72 | ```cpp 73 | #include 74 | #define endl '\n' 75 | #define int long long 76 | 77 | using namespace std; 78 | 79 | constexpr int MOD = 998244353; 80 | constexpr int MAXN = 5050; 81 | int n, m; 82 | int dp[2 * MAXN], sum[2 * MAXN]; 83 | 84 | void solve() 85 | { 86 | cin >> n >> m; 87 | for (int i = m; i >= -m; i--) 88 | { 89 | dp[i + m] = 1; 90 | sum[i + m] = sum[i + 1 + m] + 1; 91 | } 92 | for (int i = 2; i <= n; i++) 93 | { 94 | for (int j = m; j >= -m; j--) 95 | { 96 | if (j >= 0) 97 | dp[j + m] = sum[j]; 98 | else 99 | dp[j + m] = sum[m - j]; 100 | } 101 | for (int j = m; j >= -m; j--) 102 | sum[j + m] = (sum[j + m + 1] + dp[j + m]) % MOD; 103 | // for (int j = m; j >= -m; j--) 104 | // cout << sum[j + m] - sum[j + m + 1] << " \n"[j == -m]; 105 | } 106 | cout << sum[0] << endl; 107 | } 108 | 109 | signed main() 110 | { 111 | ios::sync_with_stdio(false); 112 | cin.tie(0); 113 | cout.tie(0); 114 | int t = 1; 115 | // cin >> t; 116 | while (t--) 117 | solve(); 118 | return 0; 119 | } 120 | ``` 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /040~049/048【题目】Ignore Operations.md: -------------------------------------------------------------------------------- 1 | **Monoxer Programming Contest 2022(AtCoder Beginner Contest 249)** 2 | 3 | F - Ignore Operations 4 | 5 | https://atcoder.jp/contests/abc249/tasks/abc249_f 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | Takahashi has an integer $x$. Initially, $x = 0$. 16 | 17 | There are $N$ operations. The $i$-th operation $(1 \leq i \leq N)$ is represented by two integers $t_i$ and $y_i$ as follows: 18 | 19 | - If $t_i = 1$, replace $x$ with $y_i$. 20 | - If $t_i = 2$, replace $x$ with $x + y_i$. 21 | 22 | Takahashi may skip any number between $0$ and $K$ (inclusive) of the operations. When he performs the remaining operations once each without changing the order, find the maximum possible final value of $x$ 23 | 24 | ### Constraints 25 | 26 | - $1 \leq N \leq 2 \times 10^5$ 27 | - $0 \leq K \leq N$ 28 | - $t_i \in \{1,2\} \, (1 \leq i \leq N)$ 29 | - $|y_i| \leq 10^9 \, (1 \leq i \leq N)$ 30 | - All values in input are integers. 31 | 32 | ------ 33 | 34 | ### Input 35 | 36 | Input is given from Standard Input in the following format: 37 | 38 | > $N$ $K$ 39 | > $t_1$ $y_1$ 40 | > $\vdots$ 41 | > $t_N$ $y_N$ 42 | 43 | ### Output 44 | 45 | Print the answer. 46 | 47 | ------ 48 | 49 | ### Sample Input 1 50 | 51 | > 5 1 52 | > 2 4 53 | > 2 -3 54 | > 1 2 55 | > 2 1 56 | > 2 -3 57 | 58 | ### Sample Output 1 59 | 60 | > 3 61 | 62 | If he skips the $5$-th operation, $x$ changes as $0 \rightarrow 4 \rightarrow 1 \rightarrow 2 \rightarrow 3$, so $x$ results in $3$. This is the maximum. 63 | 64 | ------ 65 | 66 | ### Sample Input 2 67 | 68 | > 1 0 69 | > 2 -1000000000 70 | 71 | ### Sample Output 2 72 | 73 | > -1000000000 74 | 75 | ------ 76 | 77 | ### Sample Input 3 78 | 79 | > 10 3 80 | > 2 3 81 | > 2 -1 82 | > 1 4 83 | > 2 -1 84 | > 2 5 85 | > 2 -9 86 | > 2 2 87 | > 1 -6 88 | > 2 5 89 | > 2 -3 90 | 91 | ### Sample Output 3 92 | 93 | > 15 94 | 95 | ### 我的笔记 96 | 97 | 对于操作 $1$,需要知道的性质是:如果选择了该操作的数字,那么该操作之前的所有操作都是无意义的,不会影响结果。 98 | 99 | **反向遍历所有操作:** 100 | 101 | 对于操作 $2$,如果数字 $\geq0$,那么当然收下,加到 $added$ 里面。如果数字 $<0$,那么暂存在一个大根堆内。 102 | 103 | 对于操作 $1$,如果不跳过该操作,计算一次 $ans=\max(ans,y_i+added)$。然后再跳过该操作,$K$ 减一。 104 | 105 | 然后需要判断堆的大小。 106 | 107 | 如果堆的大小 $\leq$ 当前还能跳过的次数,那么就可以把所有的都跳过,避免加负数到 $added$ 内。如果堆的大小 $>$ 当前还能跳过的次数,那么就只能加最大的负数到 $added$ 内。 108 | 109 | 注意如果结束时 $K>=0$,还需计算一次 $ans=\max(ans,y_i+added)$,防止初始为 $0$ 的情况遗漏。 110 | 111 | ### 代码 112 | 113 | ```cpp 114 | #include 115 | 116 | using namespace std; 117 | 118 | const int MAXN = 2e5 + 10; 119 | int N, K; 120 | int t[MAXN], y[MAXN]; 121 | priority_queue pque; 122 | 123 | int main() 124 | { 125 | cin >> N >> K; 126 | for (int i = N; i > 0; i--) 127 | cin >> t[i] >> y[i]; 128 | long long ans = INT64_MIN, added = 0; 129 | for (int i = 1; i <= N; i++) 130 | { 131 | if (t[i] == 2) 132 | { 133 | if (y[i] >= 0) 134 | added += y[i]; 135 | else 136 | pque.push(y[i]); 137 | } 138 | else 139 | { 140 | ans = max(ans, y[i] + added); 141 | K--; 142 | } 143 | while (pque.size() > K) 144 | { 145 | added += pque.top(); 146 | pque.pop(); 147 | } 148 | if (K < 0) 149 | break; 150 | } 151 | if (K >= 0) 152 | ans = max(ans, added); 153 | cout << ans << endl; 154 | return 0; 155 | } 156 | ``` 157 | 158 | -------------------------------------------------------------------------------- /120~129/129【题目】Circle of Mistery.md: -------------------------------------------------------------------------------- 1 | **2023牛客暑期多校训练营5** 2 | 3 | B - Circle of Mistery 4 | 5 | https://ac.nowcoder.com/acm/contest/57359/B 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 考虑长度为 $n$ 的每个排列 $P=\left\{p_1, p_2, \cdots, p_n\right\}$,并给定权值数组 $\{w\}_{i=1}^n$ 和权值阈值 $k$,若 $P$ 中存在一个任意长度的置换环 $\left\{a_1, a_2, \cdots, a_l\right\}$ 满足 $\sum_{i=1}^l w_{a_i} \geq k$,则考虑统计该排列 $P$ 的逆序对数。问符合条件的排列中最小逆序对数目是多少。 12 | 13 | $1 \leq n \leq 10^3,-10^6 \leq w_i, k \leq 10^6$ 。 14 | 15 | ## 题解 16 | 17 | 首先,需要考虑选择了 $\left\{a_1, a_2, \cdots, a_x\right\}$ 这些元素构造置换环,剩下的不在置换环中的元素该如何排布。观察可以得到,若要让逆序对最小化,除了构造的环内的元素外,其他的位置的元素我们都让它满足 $p_i=i$ 即处于原位。 18 | 19 | ------ 20 | 21 | 然后,需要考虑选择了 $\left\{a_1, a_2, \cdots, a_x\right\}$ 这些元素构造置换环,这些元素该怎么成环。为了保证逆序对最小化,置换环中尽量保持顺序,一种可行的方式是 $p_{a_1}=a_{2},\;p_{a_2}=a_3,\;\dots,p_{a_{x-1}}=a_x,\;p_{a_x}=a_1$,还有一种最大值在第一个的其实是一样的。 22 | 23 | ------ 24 | 25 | 现在,根据上面的策略,如果我们选择了 $\left\{a_1, a_2, \cdots, a_x\right\}$ 这些元素构造置换环,那么逆序对的数量就是确定的了。例如下面这个例子,下划线代表选中在置换环中的元素,为了方便,记置换环起点 $i$(本例为 $2$),终点为 $j$(本例为 $8$),环长度 $l$(本例为 $4$): 26 | $$ 27 | 1,\underline5,3,4,\underline7,6,\underline8,\underline2,9 28 | $$ 29 | 那么对于位于最后的最小值(本例中的 $2$),它与置换环中的所有其他元素(本例中的 $5,7,8$)都逆序,个数为 $l-1$。还有处于置换环区间里的满足 $p_i=i$ 的元素,它们每个会产生 $2$ 个逆序,一个是与右侧的最小值(本例中的 $2$),一个是与左侧的错位值(本例中 $5,7$),个数共 $2\cdot(j-i+1-l)$. 30 | 31 | 综上,给定一个置换环元素集合,它的逆序对数目为:$l-1+2\cdot(j-i+1-l)=2\cdot(j-i)-l+1$. 32 | 33 | ------ 34 | 35 | 现在,问题就转换为了选出 $\left\{a_1, a_2, \cdots, a_x\right\}$ 元素,满足 $\sum_{i=1}^l w_{a_i} \geq k$ 的同时使 $2\cdot(j-i)-l+1$ 最小化. 36 | 37 | 注意到逆序对数目这个式子,对于一个固定区间 $[i,j]$,如果选择的个数 $l$ 越多逆序对越少。由于 $n\leq 10^3$,那么区间 $[i,j]$ 可以用双重循环解决,只需要考虑的是给定一个区间,如何尽量选更多的元素。时间复杂度 $O(n^2)$ 38 | 39 | 考虑贪心思想,要求求和最大的话,对于 $\geq0$ 的元素必选,而对于 $<0$ 的元素,从最大的开始选,一直尝试选到 $k$ 不成立为止。如此就能保证 $[i,j]$ 区间中选出尽量多的元素,然后更新答案即可。 40 | 41 | 可用一个 `multiset` 储存元素并维护大小,取的时候从最大开始往小取。时间复杂度 $O(\log n)$ 42 | 43 | 总时间复杂度:$O(n^2\log n)$. 44 | 45 | ----- 46 | 47 | 注意,无法取任何数成环是代表不成立,即代码中出现 $l=0$,那是不成立的状态,不能用 $2\cdot(j-i)-l+1$ 把答案更新了,需要特判跳过。 48 | 49 | ## 代码 50 | 51 | ```cpp 52 | #include 53 | #define endl '\n' 54 | #define int long long 55 | 56 | using namespace std; 57 | 58 | void solve() 59 | { 60 | int n, k; 61 | cin >> n >> k; 62 | vector w(n + 10); 63 | for (int i = 1; i <= n; i++) 64 | cin >> w[i]; 65 | int ans = INT64_MAX; 66 | for (int i = 1; i <= n; i++) 67 | { 68 | int sum = 0, cnt = 0; 69 | multiset ms; 70 | for (int j = i; j <= n; j++) 71 | { 72 | if (w[j] < 0) 73 | { 74 | ms.insert(w[j]); 75 | } 76 | else 77 | { 78 | sum += w[j]; 79 | cnt++; 80 | } 81 | if (sum >= k) 82 | { 83 | int tsum = sum, tcnt = cnt; 84 | for (auto ti = ms.rbegin(); ti != ms.rend(); ++ti) 85 | { 86 | if (tsum + *ti < k) 87 | break; 88 | tsum += *ti; 89 | tcnt++; 90 | } 91 | if (tcnt) 92 | ans = min(ans, 2 * (j - i) - tcnt + 1); 93 | } 94 | } 95 | } 96 | if (ans == INT64_MAX) 97 | cout << -1 << endl; 98 | else 99 | cout << ans << endl; 100 | } 101 | 102 | signed main() 103 | { 104 | ios::sync_with_stdio(false); 105 | cin.tie(0); 106 | cout.tie(0); 107 | int t = 1; 108 | // cin >> t; 109 | while (t--) 110 | solve(); 111 | return 0; 112 | } 113 | ``` 114 | 115 | -------------------------------------------------------------------------------- /020~029/024【题目】XOR to All.md: -------------------------------------------------------------------------------- 1 | **AtCoder Regular Contest 135** 2 | 3 | C - XOR to All 4 | 5 | https://atcoder.jp/contests/arc135/tasks/arc135_c 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given a sequence of non-negative integers $A = (A_1, \ldots, A_N)$. You can do the operation below any number of times (possibly zero). 16 | 17 | - Choose an integer $x\in \{A_1,\ldots,A_N\}$. 18 | - Replace $A_i$ with $A_i\oplus x$ for every $i$ ($\oplus$ denotes the bitwise $\mathrm{XOR}$ operation). 19 | 20 | Find the maximum possible value of $\sum_{i=1}^N A_i$ after your operations. 21 | 22 | ### Constraints 23 | 24 | - $1\leq N \leq 3\times 10^{5}$ 25 | - $0\leq A_i < 2^{30}$ 26 | 27 | ------ 28 | 29 | ### Input 30 | 31 | Input is given from Standard Input from the following format: 32 | 33 | > $N$ 34 | > $A_1$ $\ldots$ $A_N$ 35 | 36 | ### Output 37 | 38 | Print the maximum possible value of $\sum_{i=1}^N A_i$ after your operations. 39 | 40 | ------ 41 | 42 | ### Sample Input 1 43 | 44 | > 5 45 | > 1 2 3 4 5 46 | 47 | ### Sample Output 1 48 | 49 | > 19 50 | 51 | Here is a sequence of operations that achieves $\sum_{i=1}^N A_i = 19$. 52 | 53 | - Initially, the sequence $A$ is $(1,2,3,4,5)$. 54 | - An operation with $x = 1$ changes it to $(0,3,2,5,4)$. 55 | - An operation with $x = 5$ changes it to $(5,6,7,0,1)$, where $\sum_{i=1}^N A_i = 5 + 6 + 7 + 0 + 1 = 19$. 56 | 57 | ------ 58 | 59 | ### Sample Input 2 60 | 61 | > 5 62 | > 10 10 10 10 10 63 | 64 | ### Sample Output 2 65 | 66 | > 50 67 | 68 | Doing zero operations achieves $\sum_{i=1}^N A_i = 50$. 69 | 70 | ------ 71 | 72 | ### Sample Input 3 73 | 74 | > 5 75 | > 3 1 4 1 5 76 | 77 | ### Sample Output 3 78 | 79 | > 18 80 | 81 | ### 我的笔记 82 | 83 | 本题中的 "You can do the operation below any number of times (possibly zero)." 和 "Sample 1" 就是个迷惑人的幌子,说是可以进行任意次异或操作,然而结果等价于只进行一次异或操作,因为:$(A_x\ \mathrm{XOR}\ A_i)\ \mathrm{XOR}\ (A_y\ \mathrm{XOR}\ A_i)=A_x\ \mathrm{XOR}\ A_y$ 84 | 85 | 具体到示例 $1$ 的 $(1,2,3,4,5)$,先用 $1$ 异或得到 $(0,3,2,5,4)$,再用 $5$ 异或得到 $(5,6,7,0,1)$,这俩步其实等价于直接用 $4$ 异或($4$ 是第二步 $5$ 原来对应的那个数) 86 | 87 | 所以该题简化为:有 $N$ 个数,在其中选 $1$ 个数(可以不选),用这个数与这 $N$ 个数异或,将结果求和,求和的最大值。 88 | 89 | 然后直接暴力求解一个个异或再求和肯定是不可行的,要整体来操作。 90 | 91 | 本题异或、求和都可以看成按位操作,所以可以直接统计出每一位 $0$ 和 $1$ 的数量,如果异或的那个数该位为 $0$,那么 $0、1$ 不翻转,如果是 $1$ 那么 $0、1$ 翻转,统计一次需要循环 $30$ 次(因为数字不大于 $2^{30}$)。统计了一位之后就可以把这一位算出来,第一位乘 $2^{0}$,第二位乘 $2^{1}$,第三位乘 $2^{2}$,$\dots$,第三十位乘 $2^{29}$,这样就只花了 $30$ 次循环就求出了一次和。总共有 $N$ 个数字,用每个数字都试一遍,然后找到最大值即可,时间复杂度 $O(30\times N)$ ,$1\leq N \leq 3\times 10^{5}$,因此可以 AC. 92 | 93 | ### 代码 94 | 95 | ```cpp 96 | #include 97 | 98 | using namespace std; 99 | 100 | int main(void) 101 | { 102 | ios::sync_with_stdio(false); 103 | cin.tie(0); 104 | int N; 105 | cin >> N; 106 | vector A(N); 107 | vector bit(30); 108 | for (int i = 0; i < N; i++) 109 | { 110 | cin >> A[i]; 111 | int t = A[i]; 112 | for (int i = 0; t; i++, t >>= 1) 113 | { 114 | bit[i] += t & 1; 115 | } 116 | } 117 | long long sum = accumulate(A.begin(), A.end(), 0LL); 118 | for (int i = 0; i < N; i++) 119 | { 120 | long long cur_sum = 0, d = 1; 121 | int t = A[i], bit_sum; 122 | for (int j = 0; j < 30; j++) 123 | { 124 | if (t & 1) 125 | { 126 | bit_sum = N - bit[j]; 127 | } 128 | else 129 | { 130 | bit_sum = bit[j]; 131 | } 132 | t >>= 1; 133 | cur_sum += d * bit_sum; 134 | d <<= 1; 135 | } 136 | sum = max(sum, cur_sum); 137 | } 138 | cout << sum << endl; 139 | return 0; 140 | } 141 | ``` 142 | 143 | -------------------------------------------------------------------------------- /001~009/008【算法】深度优先搜索、广度优先搜索.md: -------------------------------------------------------------------------------- 1 | 搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。 2 | 3 | **深度优先搜索 (Depth First Search, DFS)** 4 | 5 | **广度优先搜索 (Breadth First Search, BFS)** 6 | 7 | 8 | 9 | # 深度优先搜索 (DFS) 10 | 11 | ## 搜索方式 12 | 13 | 深度优先搜索,从名字上来看就是搜索深度更优先的搜索方式。它会“执着”地走到头,只有无路可走时,它才会后退(回溯)一步,寻找新的路径。若用 DFS 来遍历下面这颗树,那么下图节点上的数字便是 DFS 遍历的先后顺序,我们接下来就具体描述下该顺序: 14 | 15 | 16 | 17 | - 向深搜索 $1\rightarrow2\rightarrow3$,无路可走。 18 | - 回溯一步 $2\leftarrow3$,有新的路线。 19 | - 向深搜索 $2\rightarrow4\rightarrow5\rightarrow6$,无路可走。 20 | - 回溯一步 $5\leftarrow6$,没有新路线,再回溯一步 $4\leftarrow5$,有新的路线。 21 | - 向深搜索 $4\rightarrow7$,无路可走。 22 | - 回溯一步 $4\leftarrow7$,没有新路线,再回溯一步 $2\leftarrow4$,没有新路线,再回溯一步 $1\leftarrow2$,有新的路线。 23 | - $\cdots\cdots$ 24 | - 所有节点遍历完成 25 | 26 | 通过例子,我们大概已经明白 DFS 的搜索方式了。当然上面只是例子,DFS 能处理的并不只是树,也能够遍历图,原理是一样的。 27 | 28 | ## 时空复杂度 29 | 30 | DFS 俗称暴力搜索,因为它遍历了所有可能情况,因此它的时间复杂度较高,能处理的数据规模较小。 31 | 32 | 但 DFS 每次搜索时只记录一条路径的信息,因此空间复杂度较低,为搜索深度。 33 | 34 | ## 代码模板 35 | 36 | ```cpp 37 | type dfs(type x, ... ) // 可以存在多个变量 38 | { 39 | if( ... ) // 达成目标,找到答案 40 | { 41 | ... // 输出答案或判断最优解等等 42 | return; 43 | } 44 | if( ... ) // 达到搜索边界(即到边界了还没搜到,有时没有此步骤) 45 | { 46 | return; 47 | } 48 | for( ... ) // 遍历所有子节点 49 | { 50 | if( ... ) // 可以转移状态,一般用标志变量判断 51 | { 52 | ... // 修改标志变量,表明此节点不可转移 53 | dfs( ... ) // 搜索子节点,经常为x+1 54 | ... // 还原标志变量,表面此节点可转移 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## 典型题型 61 | 62 | 洛谷P1706 - 全排列问题: https://www.luogu.com.cn/problem/P1706 63 | 64 | 洛谷P1219 - [USACO1.5]八皇后 Checker Challenge: https://www.luogu.com.cn/problem/P1219 65 | 66 | # 广度优先搜索 (BFS) 67 | 68 | ## 搜索方式 69 | 70 | 广度(宽度)优先搜索,侧重点就在搜索的广度。它会搜索完当前深度的所有节点,才会深入一层,搜索下一层的节点。若用 BFS 来遍历下面这颗树,那么下图节点上的数字便是 BFS 遍历的先后顺序,我们接下来就具体描述下该顺序: 71 | 72 | 73 | 74 | - 遍历深度为 $1$ 的节点 $1$ 75 | - 遍历深度为 $2$ 的节点 $2,3$ 76 | - 遍历深度为 $3$ 的节点 $4,5,6,7$ 77 | - 遍历深度为 $4$ 的节点 $8,9$ 78 | - 遍历深度为 $5$ 的节点 $10$ 79 | - 所有节点遍历完成 80 | 81 | 通过例子,我们大概已经明白 BFS 的搜索方式了。可以发现一个性质就是,如果路径长度为 $1$,BFS 遍历到的节点深度就等于距起点的距离。因此可以通过 BFS 处理一些最短路问题。 82 | 83 | ## 时空复杂度 84 | 85 | BFS 也可以将所有情况遍历到,那么时间复杂度和 DFS 差不多。但 BFS 我们往往找到答案就会终止,并且可能更快找到答案,因此有时时间复杂度比 DFS 更好。 86 | 87 | BFS 在每层遍历时需要保存该层所有节点,因此空间复杂度较高。 88 | 89 | ## 代码实现模板 90 | 91 | ```cpp 92 | bool vis[MAXN]; // 标记是否搜索过,有时也可直接用depth来判断 93 | int depth[MAXN]; // 储存搜索深度,有时可能为二维数组或map 94 | queue que; // STL队列,不过数组模拟队列效率更高 95 | 96 | type bfs(type start) 97 | { 98 | que.push(start); // 起点入队 99 | depth[start] = 0; // 起点深度0 100 | vis[start] = true; // 标记起点 101 | while (!que.empty()) 102 | { 103 | type now = que.front(); // 当前节点设置为队首 104 | que.pop(); // 弹出队首 105 | if ( ... ) // 如果达到目标条件 106 | { 107 | ans = depth[now]; // 储存答案 108 | return; // 搜索结束 109 | } 110 | for( ... ) // 遍历now节点的所有子节点,可用数组表示方向 111 | { 112 | type next = ... // 计算出子节点 113 | if (!vis[next] && ... ) // 如果子节点未搜索过,且范围符合题目条件 114 | { 115 | vis[next] = true; // 标记子节点 116 | depth[next] = depth[now] + 1; // 子节点深度+1 117 | que.push(next); // 子节点入队 118 | // 有时题目还需输出具体路径,可用一个数组储存每个节点的上一个节点,然后在此处对数组赋值。输出时,从结尾递归反向输出即可获得具体的路径。 119 | } 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | ## 典型题型 126 | 127 | POJ1077 - Eight: http://poj.org/problem?id=1077 128 | 129 | POJ3984 - 迷宫问题: http://poj.org/problem?id=3984 130 | -------------------------------------------------------------------------------- /120~129/123【算法】最近公共祖先.md: -------------------------------------------------------------------------------- 1 | **最近公共祖先** (LCA, Lowest Common Ancestor):树中两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 2 | 3 | 4 | 5 | # 朴素算法 6 | 7 | ### 预处理 8 | 9 | 记录树中每个节点 $i$ 的深度 $dep_i$ 和父节点 $fa_i$。可以在 DFS 遍历整棵树的同时完成这个预处理。 10 | 11 | 时间复杂度:$O(V)$ 12 | 13 | ### 求解 14 | 15 | 给出要求解 LCA 的两个节点 $a,b$,不妨设 $dep_a\geq dep_b$. 16 | 17 | 首先,较深的节点 $a$ 需要先向上跳到和 $b$ 一样的深度: 18 | 19 | ```cpp 20 | while (dep[a] > dep[b]) 21 | a = fa[a]; 22 | ``` 23 | 24 | 跳到一样的深度后,如果 $a=b$,则说明二者的 LCA 就是 $b$,输出 $b$ 结束算法。 25 | 26 | 如果 $a\neq b$,则说明二者的 LCA 还在上层,接下来 $a$ 和 $b$ 一起往上一层层跳,直到找到公共祖先。 27 | 28 | ```cpp 29 | while (a != b) 30 | a = fa[a], b = fa[b]; 31 | ``` 32 | 33 | 时间复杂度:$O(d)$,其中 $d$ 为树的深度。对于一个随机树,它的平均深度 $d=\log V$,那么时间复杂度为 $O(\log V)$. 34 | 35 | # 倍增算法 36 | 37 | 上面朴素算法最大的问题就是跳得太慢了,每次都是一层一层得往上找。我们其实可以用倍增来找,即每次向上跳 $2^n$ 层。 38 | 39 | 以下面这个树作为例子,要求 $14$ 和 $15$ 的 LCA: 40 | 41 | ``` 42 | 1 | 1 43 | | / \ 44 | 2 | 2 3 45 | | /|\ / \ 46 | 3 | 5 6 7 8 9 47 | | / \ 48 | 4 | 10 11 49 | | | | 50 | 5 | 12 13 51 | | | | 52 | 6 | 14 15 53 | ``` 54 | 55 | - 找 $14$ 和 $15$ 往上 $8$ 层的祖先,发现二者祖先已经超出根节点了,我们看作相同,缩小距离继续。 56 | - 找 $14$ 和 $15$ 往上 $4$ 层的祖先,发现二者都为 $2$,祖先相同,缩小距离继续。 57 | - 找 $14$ 和 $15$ 往上 $2$ 层的祖先,发现二者为 $10,11$,祖先不同,向上跳后继续。 58 | - 找 $10$ 和 $11$ 往上 $1$ 层的祖先,发现二者都为 $5$,祖先相同,缩小距离继续。 59 | - 距离缩小到 $0$,结束算法。此时 $a$ 或 $b$ 的上一层祖先便是求出的 LCA. 60 | 61 | 有一点比较难理解,为啥祖先不同时往上跳,祖先相同时只缩小距离。其实这有点二分的味道: 62 | 63 | 如下图,假设 $a,b$ 两点的 LCA 为 $11$,那么首先跳 $16$ 发现祖先相同,于是下一步跳 $8$ 发现祖先不同...... 其实就是二分。 64 | 65 | ``` 66 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 67 | =============================================√ 68 | =====================x 69 | ===========√ 70 | =====x 71 | ==√ 72 | ``` 73 | 74 | 当然,倍增算法首先也要把两个点先跳到一层,跳的方法也是使用倍增来完成。 75 | 76 | 例如 $a$ 比 $b$ 深 $7$ 层的话,就会跳 $4+2+1$,最后 $a,b$ 到同一层。 77 | 78 | ### 预处理 79 | 80 | 记录树中每个节点 $i$ 的深度 $dep_i$ 和向上跳 $2^j$ 层的父节点 $fa(i,j)$。可以在 DFS 遍历整棵树的同时完成这个预处理。 81 | 82 | 父节点的维护其实算作一种动态规划,转移方程为:$fa(i,j+1)=fa(fa(i,j),j)$ 83 | 84 | 另外,可以预处理出来所有的 $\lfloor \log_2 x\rfloor+1$,这个是一个常数级别优化。递推方式为:`mylog2[i] = mylog2[i - 1] + (1 << mylog2[i - 1] == i);` 85 | 86 | ```cpp 87 | constexpr int MAXN = 1e6 + 10; 88 | int h[MAXN], e[MAXN], ne[MAXN], idx; 89 | int mylog2[MAXN]; // \lfloor log_{2}{x} \rfloor + 1 90 | int fa[MAXN][30], dep[MAXN]; 91 | 92 | void init() 93 | { 94 | memset(h, -1, sizeof(h)); 95 | for (int i = 1; i < MAXN; i++) 96 | mylog2[i] = mylog2[i - 1] + (1 << mylog2[i - 1] == i); 97 | } 98 | 99 | void dfs(int now, int father) 100 | { 101 | fa[now][0] = father; 102 | dep[now] = dep[father] + 1; 103 | for (int i = 0; i < mylog2[dep[now]]; i++) 104 | fa[now][i + 1] = fa[fa[now][i]][i]; 105 | for (int i = h[now]; i != -1; i = ne[i]) 106 | if (e[i] != father) 107 | dfs(e[i], now); 108 | } 109 | ``` 110 | 111 | 时间复杂度:$O(V\log d)$,其中 $d$ 为树的深度。 112 | 113 | ### 计算 114 | 115 | ```cpp 116 | int lca(int a, int b) 117 | { 118 | if (dep[a] < dep[b]) 119 | swap(a, b); // make a >= b 120 | while (dep[a] > dep[b]) 121 | a = fa[a][mylog2[dep[a] - dep[b]] - 1]; 122 | if (a == b) 123 | return a; 124 | for (int i = mylog2[dep[a]] - 1; i >= 0; i--) 125 | { 126 | if (fa[a][i] != fa[b][i]) 127 | { 128 | a = fa[a][i]; 129 | b = fa[b][i]; 130 | } 131 | } 132 | return fa[a][0]; 133 | } 134 | ``` 135 | 136 | 时间复杂度:$O(\log d)$,其中 $d$ 为树的深度。 137 | 138 | # 树链剖分 139 | 140 | 预处理时间复杂度:$O(n)$ 141 | 142 | 查询时间复杂度:$O(\log n)$ 143 | 144 | **具体介绍见:[树链剖分](https://io.zouht.com/130.html)** -------------------------------------------------------------------------------- /050~059/050【题目】Prefix Equality.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 250** 2 | 3 | E - Prefix Equality 4 | 5 | https://atcoder.jp/contests/abc250/tasks/abc250_e 6 | 7 | 8 | 9 | Time Limit: 4 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given integer sequences $A = (a_1,\ldots,a_N)$ and $B = (b_1,\ldots,b_N)$, each of length N*N*. 16 | 17 | For $i=1,...,Q$, answer the query in the following format. 18 | 19 | - If the set of values contained in the first $x_i$ terms of $A$, $(a_1,\ldots,a_{x_i}),$ and the set of values contained in the first $y_i$ terms of $B$, $(b_1,\ldots,b_{y_i})$, are equal, then print `Yes`; otherwise, print `No`. 20 | 21 | ### Constraints 22 | 23 | - $1 \leq N,Q \leq 2 \times 10^5$ 24 | - $1 \leq a_i,b_i \leq 10^9$ 25 | - $1 \leq x_i,y_i \leq N$ 26 | - All values in input are integers. 27 | 28 | ------ 29 | 30 | ### Input 31 | 32 | Input is given from Standard Input in the following format: 33 | 34 | > $N$ 35 | > $a_1$ $\ldots$ $a_N$ 36 | > $b_1$ $\ldots$ $b_N$ 37 | > $Q$ 38 | > $x_1$ $y_1$ 39 | > $\vdots$ 40 | > $x_Q$ $y_Q$ 41 | 42 | ### Output 43 | 44 | Print $Q$ lines. The $i$-th line should contain the response to the $i$-th query. 45 | 46 | ------ 47 | 48 | ### Sample Input 1 49 | 50 | > 5 51 | > 1 2 3 4 5 52 | > 1 2 2 4 3 53 | > 7 54 | > 1 1 55 | > 2 2 56 | > 2 3 57 | > 3 3 58 | > 4 4 59 | > 4 5 60 | > 5 5 61 | 62 | ### Sample Output 1 63 | 64 | > Yes 65 | > Yes 66 | > Yes 67 | > No 68 | > No 69 | > Yes 70 | > No 71 | 72 | Note that sets are a concept where it matters only whether each value is contained or not. 73 | For the $3$-rd query, the first $2$ terms of $A$ contain one $1$ and one $2$, while the first $3$ terms of $B$ contain one $1$ and two $2$'s. However, the sets of values contained in the segments are both $\{ 1,2 \}$, which are equal. 74 | Also, for the 66-th query, the values appear in different orders, but they are still equal as sets. 75 | 76 | ### 我的笔记 77 | 78 | 使用 $hash$ 计算出 $a_1\sim a_x$ 和 $b_1\sim b_x\;(1\leq x\leq N)$ 所含数字集合的 $hash$ 值,存到长为 $N$ 的数组 $hash\_a$、$hash\_b$ 内。 79 | 80 | 判断时,只需判断 $hash\_a[x_i]$ 是否等于 $hash\_b[y_i]$。若等于,则集合相等;不等,则集合不等。 81 | 82 | $hash$ 函数可用 $f(x)=x\cdot(x+1326)\cdot(x+9185)$,使用自然溢出法。 83 | 84 | ### 源码 85 | 86 | ```cpp 87 | #include 88 | 89 | using namespace std; 90 | 91 | const int MAXN = 2e5 + 20; 92 | int N, Q; 93 | unsigned int hash_a[MAXN], hash_b[MAXN]; 94 | 95 | int main() 96 | { 97 | ios::sync_with_stdio(false); 98 | cin.tie(0); 99 | cin >> N; 100 | set st; 101 | for (int i = 0; i < N; i++) 102 | { 103 | int tmp; 104 | cin >> tmp; 105 | if (st.find(tmp) == st.end()) 106 | { 107 | st.insert(tmp); 108 | hash_a[i + 1] = (hash_a[i] + tmp * (tmp + 1326) * (tmp + 9185)); 109 | } 110 | else 111 | { 112 | hash_a[i + 1] = hash_a[i]; 113 | } 114 | } 115 | st.clear(); 116 | for (int i = 0; i < N; i++) 117 | { 118 | int tmp; 119 | cin >> tmp; 120 | if (st.find(tmp) == st.end()) 121 | { 122 | st.insert(tmp); 123 | hash_b[i + 1] = (hash_b[i] + tmp * (tmp + 1326) * (tmp + 9185)); 124 | } 125 | else 126 | { 127 | hash_b[i + 1] = hash_b[i]; 128 | } 129 | } 130 | cin >> Q; 131 | while (Q--) 132 | { 133 | int a, b; 134 | cin >> a >> b; 135 | if (hash_a[a] == hash_b[b]) 136 | cout << "Yes" << endl; 137 | else 138 | cout << "No" << endl; 139 | } 140 | return 0; 141 | } 142 | ``` 143 | 144 | -------------------------------------------------------------------------------- /020~029/022【题目】AND and SUM.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 238** 2 | 3 | D - AND and SUM 4 | 5 | https://atcoder.jp/contests/abc238/tasks/abc238_d 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $400$ points 12 | 13 | ### Problem Statement 14 | 15 | Solve the following problem for $T$ test cases. 16 | 17 | > Given are non-negative integers $a$ and $s$. Is there a pair of non-negative integers $(x,y)$ that satisfies both of the conditions below? 18 | > 19 | > - $x\ \text{AND}\ y=a$ 20 | > - $x+y=s$ 21 | 22 | ### Constraints 23 | 24 | - $1 \leq T \leq 10^5$ 25 | - $0 \leq a,s \lt 2^{60}$ 26 | - All values in input are integers. 27 | 28 | ------ 29 | 30 | ### Input 31 | 32 | Input is given from Standard Input. The first line is in the following format: 33 | 34 | > $T$ 35 | 36 | Then, $T$ test cases follow. Each test case is in the following format: 37 | 38 | > $a$ $s$ 39 | 40 | ### Output 41 | 42 | Print $T$ lines. The $i$-th line $(1 \leq i \leq T)$ should contain `Yes` if, in the $i$-th test case, there is a pair of non-negative integers $(x,y)$ that satisfies both of the conditions in the Problem Statement, and `No` otherwise. 43 | 44 | ### Sample Input 1 45 | 46 | > 2 47 | > 1 8 48 | > 4 2 49 | 50 | ### Sample Output 1 51 | 52 | > Yes 53 | > No 54 | 55 | In the first test case, some pairs such as $(x,y)=(3,5)$ satisfy the conditions. 56 | 57 | In the second test case, no pair of non-negative integers satisfies the conditions. 58 | 59 | ------ 60 | 61 | ### Sample Input 2 62 | 63 | > 4 64 | > 201408139683277485 381410962404666524 65 | > 360288799186493714 788806911317182736 66 | > 18999951915747344 451273909320288229 67 | > 962424162689761932 1097438793187620758 68 | 69 | ### Sample Output 2 70 | 71 | > No 72 | > Yes 73 | > Yes 74 | > No 75 | 76 | ### 我的笔记 77 | 78 | 本题纯思维题,需要两个结论: 79 | 80 | 1. $(x\ \text{XOR}\ y)\ \text{AND}\ (x\ \text{AND}\ y)=0$ 81 | 82 | 2. $x+y=2\times(x\ \text{AND}\ y)+(x\ \text{XOR}\ y)$ 83 | 84 | 下面来推导一下这两个结论。 85 | 86 | **先看第一个结论:**$(x\ \text{XOR}\ y)\ \text{AND}\ (x\ \text{AND}\ y)=0$ 87 | 88 | 用具体的数来说明更形象,不妨令: 89 | $x=00110100_2$ 90 | $y=11011110_2$ 91 | 92 | 那么: 93 | $x\ \text{AND}\ y=00010100_2$ 94 | $x\ \text{XOR}\ y=11101010_2$ 95 | 96 | 观察上面两数,用通俗的话来解释: 97 | $x\ \text{AND}\ y$ 的第 $a$ 位为 $1$ 就是 $x$ 和 $y$ 两数的第 $a$ 位**同时都**为 $1$ 98 | $x\ \text{XOR}\ y$ 的第 $a$ 位为 $1$ 就是 $x$ 和 $y$ 两数的第 $a$ 位**有一个**为 $1$ 99 | 100 | 因此这两个事件不可能同时发生,即不可能两个数的同一位**既**同时为 $1$ **又**有一个为 $1$,所以第一个结论得证。 101 | 102 | **再看第二个结论:**$x+y=2\times(x\ \text{AND}\ y)+(x\ \text{XOR}\ y)$ 103 | 104 | 我们把 $x$ 和 $y$ 都为 $1$ 的那位单独提出来(对应加号前的数): 105 | $x=00110100_2=00010100_2+00100000_2$ 106 | $y=11011110_2=00010100_2+11001010_2$ 107 | 108 | 再将 $x$ 和 $y$ 相加: 109 | $\begin{aligned}x+y&=00010100_2+00100000_2+00010100_2+11001010_2\\&=2\times00010100_2+11101010_2\\&=2\times(x\ \text{AND}\ y)+(x\ \text{XOR}\ y)\end{aligned}$ 110 | 111 | 至于为什么另外两个数加起来是 $x\ \text{XOR}\ y$,那是因为把同为 $1$ 的提出去了,剩下的都是同为 $0$ 或二者之一为 $1$,加起来当然就是 $x\ \text{XOR}\ y$. 112 | 113 | **本题解法:** 114 | 115 | 本题知道: 116 | $x\ \text{AND}\ y=a$ 117 | $x+y=s$ 118 | 119 | 由第二个结论可求出 $x\ \text{XOR}\ y=s-2\times a$ 120 | 121 | 再使用第一个结论,若 $(s-2\times a)\ \text{AND}\ a=0$,则符合题意,反之不符合。 122 | 123 | 还有一个大前提,$s-2\times a$ 要 $\geq0$,否则就肯定不成立。 124 | 125 | ### 源码 126 | 127 | ```cpp 128 | #include 129 | 130 | using namespace std; 131 | 132 | int main(void) 133 | { 134 | int T; 135 | cin >> T; 136 | while (T--) 137 | { 138 | long long a, s; 139 | cin >> a >> s; 140 | long long x = s - 2 * a; 141 | if (x >= 0 && !(x & a)) 142 | cout << "Yes" << endl; 143 | else 144 | cout << "No" << endl; 145 | } 146 | return 0; 147 | } 148 | ``` 149 | 150 | -------------------------------------------------------------------------------- /020~029/027【算法】Dijkstra 算法.md: -------------------------------------------------------------------------------- 1 | **解决赋权图的单源最短路径问题:**Dijkstra (/ˈdaɪkstrəz/, 迪杰斯特拉) 算法 2 | 3 | - 不能解决负边 4 | 5 | 6 | 7 | # Dijkstra 算法 8 | 9 | ## 朴素版本 10 | 11 | ### 复杂度 12 | 13 | 时间复杂度:$O(\left|V\right|^2)$ 14 | 15 | 此为最坏情况,$\left|E\right|$ 为边数,$\left|V\right|$ 为顶点数。该版本适合稠密图,即 $\left|E\right|接近\left|V\right|^2$ 的图。 16 | 17 | ### 分析 18 | 19 | **核心思想** 20 | 21 | 以下图为例,该图为一个有向图,箭头上的数字为权值,且权值均为非负数。 22 | 23 | 假设我们已经确定了 $A\rightarrow A=0$,$A\rightarrow C=1$,称 $A,C$ 为确定点。那么我们计算出可以从确定点一步到达的点的最短距离,即 $A\rightarrow B=2,A\rightarrow D=3,A\rightarrow E=4$,称 $B,D,E$ 为待定点。 24 | 25 | 接下来,我们用贪心思想还能将待定点中距离最小的那个点转化为确定点,即 $B$ 点的距离 $2$ 可以确定是最短的。然后我们就可以用该确定点更新可到达的点,称为松弛操作。再循环上述整个过程。 26 | 27 | 28 | 29 | 以上便是 Dijkstra 算法的核心思想,其正确性证明可见文章:https://zhuanlan.zhihu.com/p/340708110。 30 | 31 | **伪代码表示** 32 | 33 | ``` 34 | dijkstra (G, dist[]) 35 | 初始化: dist[1~v]=+INF, dist[1]=0 36 | for (循环v次) 37 | t=待定点的集合中dist[t]最小的顶点 38 | 记t已确定 39 | for (j : 从t出发能到达的顶点的集合) 40 | if (以t为中介点到j的距离更短) 41 | 更新dist[j]; 42 | ``` 43 | 44 | ### 代码实现 45 | 46 | ```cpp 47 | const int MAXN = 510, INF = 0x3f3f3f3f; // INF代表无穷大 48 | int g[MAXN][MAXN]; // 邻接矩阵存图 49 | int dist[MAXN]; // 最短距离 50 | bool vis[MAXN]; // 访问情况 51 | int v, e; // v顶点数 e边数 52 | 53 | void dijkstra() 54 | { 55 | memset(dist, 0x3f, sizeof(dist)); 56 | dist[1] = 0; 57 | for (int i = 1; i <= v; i++) 58 | { 59 | int t = -1; 60 | for (int j = 1; j <= v; j++) 61 | if (!vis[j] && (t == -1 || dist[t] > dist[j])) 62 | t = j; 63 | vis[t] = true; 64 | for (int j = 1; j <= v; j++) 65 | dist[j] = min(dist[j], dist[t] + g[t][j]); 66 | } 67 | } 68 | ``` 69 | 70 | ## 堆优化版本 71 | 72 | ### 复杂度 73 | 74 | 时间复杂度:$O((\left|E\right|+\left|V\right|)\log\left|V\right|)$ 75 | 76 | 此为最坏情况,$\left|E\right|$ 为边数,$\left|V\right|$ 为顶点数。该版本适合稀疏图,即 $\left|E\right|\ll\left|V\right|^2$ 的图。 77 | 78 | ### 分析 79 | 80 | 朴素版本中,每次迭代都有一个查找距离最小顶点的过程,并且对所有点尝试进行了松弛操作,共需要 $2\left|V\right|$ 次循环。 81 | 82 | 针对查找最小值这一操作,二叉堆是一个很好的数据结构。我们建立一个小顶堆,即可在 $O(1)$ 时间内取得最小值。 83 | 84 | 针对松弛操作,我们将存图方式改为邻接表,所以我们就可以只更新与该点相连的顶点,效率更高。 85 | 86 | ### 代码实现 87 | 88 | 使用 STL 库中的优先队列作为堆,由于其没有更新元素的功能,所以节点距离更新后,无法更新堆中的数据。但可以用冗余的方法,节点距离更新后直接插入新距离,旧距离仍然储存在堆中。由于堆顶最小,我们可以保证新距离最先取到,之后若发现取到了旧距离,直接忽略即可(参见循环中的 continue 语句)。 89 | 90 | ```cpp 91 | #include 92 | 93 | using namespace std; 94 | 95 | typedef pair PII; 96 | const int MAXN = 100010, INF = 0x3f3f3f3f; // INF代表无穷大 97 | int E, V, S; // E边数,V顶点数,S起点 98 | vector edge[MAXN]; // 储存连接关系,二元组为(权值,终点) 99 | priority_queue, greater> pque; // 储存节点,节点距离小的在堆顶 100 | int dist[MAXN]; // 储存节点距离 101 | bool vis[MAXN]; // 是否已经访问过该节点的标志 102 | 103 | void dijkstra() 104 | { 105 | memset(dist, 0x3f, sizeof(dist)); 106 | dist[1] = 0; 107 | pque.push({dist[1], 1}); 108 | while (!pque.empty()) 109 | { 110 | PII cur = pque.top(); 111 | pque.pop(); 112 | if (vis[cur.second]) 113 | continue; 114 | vis[cur.second] = true; 115 | for (auto next : edge[cur.second]) 116 | { 117 | if (dist[next.second] > dist[cur.second] + next.first) 118 | { 119 | dist[next.second] = dist[cur.second] + next.first; 120 | if (!vis[next.second]) 121 | pque.push({dist[next.second], next.second}); 122 | } 123 | } 124 | } 125 | } 126 | ``` 127 | -------------------------------------------------------------------------------- /050~059/059【题目】I Hate Non-integer Number.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 262** 2 | 3 | D - I Hate Non-integer Number 4 | 5 | https://atcoder.jp/contests/abc262/tasks/abc262_d 6 | 7 | 8 | 9 | Time Limit: 2.5 sec / Memory Limit: 1024 MB 10 | 11 | Score : $400$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given a sequence of positive integers $A=(a_1,\ldots,a_N)$ of length $N$. 16 | There are $(2^N-1)$ ways to choose one or more terms of $A$. How many of them have an integer-valued average? Find the count modulo $998244353$. 17 | 18 | ### Constraints 19 | 20 | - $1 \leq N \leq 100$ 21 | - $1 \leq a_i \leq 10^9$ 22 | - All values in input are integers. 23 | 24 | ### Input 25 | 26 | Input is given from Standard Input in the following format: 27 | 28 | > $N$ 29 | > $a_1$ $\ldots$ $a_N$ 30 | 31 | ### Output 32 | 33 | Print the answer. 34 | 35 | ### Sample Input 1 36 | 37 | > 3 38 | > 2 6 2 39 | 40 | ### Sample Output 1 41 | 42 | > 6 43 | 44 | For each way to choose terms of $A$, the average is obtained as follows: 45 | 46 | - If just $a_1$ is chosen, the average is $\frac{a_1}{1}=\frac{2}{1} = 2$, which is an integer. 47 | - If just $a_2$ is chosen, the average is $\frac{a_2}{1}=\frac{6}{1} = 6$, which is an integer. 48 | - If just $a_3$ is chosen, the average is $\frac{a_3}{1}=\frac{2}{1} = 2$, which is an integer. 49 | - If $a_1$ and $a_2$ are chosen, the average is $\frac{a_1+a_2}{2}=\frac{2+6}{2} = 4$, which is an integer. 50 | - If $a_1$ and $a_3$ are chosen, the average is $\frac{a_1+a_3}{2}=\frac{2+2}{2} = 2$, which is an integer. 51 | - If $a_2$ and $a_3$ are chosen, the average is $\frac{a_2+a_3}{2}=\frac{6+2}{2} = 4$, which is an integer. 52 | - If $a_1$, $a_2$, and $a_3$ are chosen, the average is $\frac{a_1+a_2+a_3}{3}=\frac{2+6+2}{3} = \frac{10}{3}$, which is not an integer. 53 | 54 | Therefore, $6$ ways satisfy the condition. 55 | 56 | ------ 57 | 58 | ### Sample Input 2 59 | 60 | > 5 61 | > 5 5 5 5 5 62 | 63 | ### Sample Output 2 64 | 65 | > 31 66 | 67 | Regardless of the choice of one or more terms of $A$, the average equals $5$. 68 | 69 | ### 我的笔记 70 | 71 | 这个动态规划比较重要的点就是将不同分母的情况分开讨论,如果分母确定,那么只需要知道余数,就可以进行转移。但如果将不同分母的情况混在一起讨论,那就需要储存不同选法的和,而和的范围非常宽广,不可能使用数组储存下来,因此无法实现。 72 | 73 | #### 状态表示 74 | 75 | $dp[i][j][k]:=$ 讨论前 $i$ 个数,选择 $j$ 个数时,和的余数为 $k$ 的情况。 76 | 77 | #### 状态转移 78 | 79 | 不选 $a[j]$ 时:$dp[j+1][k][l]\text{+=}dp[j][k][l]$ 80 | 81 | 选 $a[j]$ 时:$dp[j+1][k+1][(l+a[k])\%i]\text{+=}dp[j][k][l]$ 82 | 83 | ### 代码 84 | 85 | ```cpp 86 | #include 87 | 88 | using namespace std; 89 | 90 | const int MOD = 998244353; 91 | const int MAXN = 110; 92 | int N, a[MAXN]; 93 | int ans = 0; 94 | 95 | int main() 96 | { 97 | cin >> N; 98 | for (int i = 0; i < N; i++) 99 | cin >> a[i]; 100 | for (int i = 1; i <= N; i++) // 分母为i 101 | { 102 | vector dp(N + 1, vector(i + 1, vector(i, 0))); 103 | dp[0][0][0] = 1; 104 | for (int j = 0; j < N; j++) // 前j+1个数 105 | { 106 | for (int k = 0; k <= i; k++) // 选k个数 107 | { 108 | for (int l = 0; l < i; l++) // 除i余l 109 | { 110 | dp[j + 1][k][l] += dp[j][k][l]; 111 | dp[j + 1][k][l] %= MOD; 112 | if (k != i) 113 | { 114 | dp[j + 1][k + 1][(l + a[j]) % i] += dp[j][k][l]; 115 | dp[j + 1][k + 1][(l + a[j]) % i] %= MOD; 116 | } 117 | } 118 | } 119 | } 120 | ans += dp[N][i][0]; 121 | ans %= MOD; 122 | } 123 | cout << ans << endl; 124 | return 0; 125 | } 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /001~009/005【算法】埃氏筛、欧拉筛.md: -------------------------------------------------------------------------------- 1 | **检定素数的算法:**埃氏筛 (埃拉托斯特尼筛法, Sieve of Eratosthenes)、欧拉筛 (线性筛, Euler's sieve) 2 | 3 | 4 | 5 | # 埃氏筛 6 | 7 | ## 复杂度 8 | 9 | - 时间复杂度:$O(n\log\log n)$ 10 | - 空间复杂度:$O(n)$ 11 | 12 | $n$ 为计算范围 13 | 14 | ## 代码实现 15 | 16 | 结果为一个范围从 $0\sim\text{MAXN}$ 的 bool 数组 is_prime,每一位储存该数字是否是素数。 17 | 18 | ```cpp 19 | const int MAXN = 1e5 + 10; 20 | bool is_prime[MAXN]; // is_prime储存该数是否为素数 21 | 22 | void init_prime() 23 | { 24 | memset(is_prime, true, sizeof(is_prime)); 25 | is_prime[0] = is_prime[1] = false; // 特判0、1不是素数 26 | for (int i = 2; i < MAXN; i++) // 使用埃氏筛处理>1的情况 27 | if (is_prime[i]) 28 | for (int j = 2; j * i < MAXN; j++) 29 | is_prime[i * j] = false; 30 | } 31 | ``` 32 | 33 | ## 算法分析 34 | 35 | #### 原理 36 | 37 | 从 $2$ 开始,如果该数为素数,就筛掉这个数的所有倍数。 38 | 39 | 这个思路非常简单易懂,因此埃氏筛比下面要讲的欧拉筛好写。 40 | 41 | #### 举例 42 | 43 | $2$ 筛除 $4、6、8、10、12、14、16\cdots$ 44 | $3$ 筛除 $6、9、12、15、18、21、24\cdots$ 45 | $4$ 是合数,不筛除其他数字 46 | $5$ 筛除 $10、15、20、25、30、35、40\cdots$ 47 | $\vdots$ 48 | 49 | # 欧拉筛 50 | 51 | ## 时间复杂度 52 | 53 | - 时间复杂度:$O(n)$ 54 | - 空间复杂度:$O(n)$ 55 | 56 | $n$ 为计算范围 57 | 58 | ## 代码实现 59 | 60 | 结果为一个下标 $0\sim\text{MAXN}$ 的 bool 数组 not_prime,每一位储存该数字是否不是素数。一个下标 $0\sim\text{primesize}$ 的 int 数组 prime,里面从小到大存放着范围 $[0,\text{MAXN}]$ 的素数。 61 | 62 | 为什么这里使用 not_prime 而不是 is_prime,其实只是为了省去全部 memset 为 true 的时间,加快效率。不过实际也不会有很大的效率区别,如果自己不习惯的话可以也改成 is_prime 版本。 63 | 64 | ```cpp 65 | // prime储存素数序列,primesize即素数数量,not_prime储存该数是否不是素数 66 | const int MAXN = 1e5 + 10; 67 | int prime[MAXN], primesize = 0; 68 | bool not_prime[MAXN]; 69 | 70 | void init_prime() 71 | { 72 | not_prime[0] = not_prime[1] = true; // 特判0、1不是素数 73 | for (int i = 2; i < MAXN; i++) // 使用欧拉筛处理>1的情况 74 | { 75 | if (!not_prime[i]) 76 | prime[primesize++] = i; 77 | for (int j = 0; j < primesize && i * prime[j] < MAXN; j++) 78 | { 79 | not_prime[i * prime[j]] = true; 80 | if (i % prime[j] == 0) 81 | break; 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## 算法分析 88 | 89 | 埃氏筛会将一个数多次筛除,如 $30$ 会被 $2,3,5$ 筛除三次,浪费一定的时间。 90 | 欧拉筛只会用该数的最小质因子筛除该数,例如 $30$ 只会被 $2$ 筛除一次,节约时间。 91 | 92 | 该算法的灵魂就是每一步停止筛除的判断,对应到代码即 `if (i % prime[j] == 0) break;` 这一条。 93 | 94 | #### 原理 95 | 96 | $\text{prime}$ 数组中的素数是递增的,当 $i$ 是 $\text{prime}_j$ 的倍数,那么 $i\times \text{prime}_{j+1}$ 这个合数肯定**已经被** $\text{prime}_j$ 乘以某个数筛掉。因此当我们发现 $i$ 是 $\text{prime}_j$ 的倍数时,就可以 break 掉当前循环停止筛出。 97 | 98 | 简单证明如下: 99 | 100 | 因为 $\text{prime}_j\mid i$,即 $i = \text{prime}_j\times k\ (k\in\mathbb{Z})$ ,那么: 101 | $$ 102 | i \times \text{prime}_{j+1} = (\text{prime}_j\times k) \times \text{prime}_{j+1} = k' \times \text{prime}_j 103 | $$ 104 | 接下去的素数同理,所以不用筛下去了。 105 | 106 | 因此,在满足 $\text{prime}_j \mid i$ 这个条件之前以及第一次满足该条件时,$\text{prime}_j$ 必定是 $\text{prime}_j \times i$ 的最小因子。 107 | 108 | #### 举例 109 | 110 | 模拟该算法的操作:从 $2$ 开始 111 | 112 | - $2$ 为素数,将其加入 prime[ ],当前 prime[1] = {2} 113 | - 筛除 $2*2$ 114 | - $2\bmod2=0$,停止筛除 115 | - $3$ 为素数,将其加入 prime[ ],当前 prime[2] = {2, 3} 116 | - 筛除 $3*2$ 117 | - 筛除 $3*3$ 118 | - $3\bmod3=0$,停止筛除 119 | - $4$ 为合数 120 | - 筛除 $4*2$ 121 | - $4\bmod2=0$,停止筛除 122 | - $5$ 为素数,将其加入 prime[ ],当前 prime[3] = {2, 3, 5} 123 | - 筛除 $5*2$ 124 | - 筛除 $5*3$ 125 | - 到达 primesize,停止筛除 126 | - $6$ 为合数 127 | - 筛除 $6*2$ 128 | - $6\bmod2=0$,停止筛除 129 | 130 | ​ $\vdots$ 131 | 132 | 就拿在 $4\bmod2=0$ 时停止筛除这个情况举例,当处于这个情况时,如果我们不停下,下一个要筛的就是 $4\times3$,但是因为 $4$ 里面包含有 $2$,就说明 $4\times3$ 一定能被 $2\times6$ 筛去,再往后 $4\times4$ 一定能被 $2\times8$ 筛去,也就是当满足这个条件后,应当停止筛除。 133 | 134 | # 时间复杂度对比 135 | 136 | 一个为 $O(n\log\log n)$,一个为 $O(n)$,虽说从时间复杂度上看,线性筛绝对占优势,如下图(假设底数为 $e$) 137 | 138 | 139 | 140 | 但在较小数据范围时,优势较小。而且线性筛内部包含有取模运算,在特定数据范围内可能速度不如埃氏筛。 141 | -------------------------------------------------------------------------------- /050~059/055【题目】Gift Tax.md: -------------------------------------------------------------------------------- 1 | **AtCoder Regular Contest 144** 2 | 3 | B - Gift Tax 4 | 5 | https://atcoder.jp/contests/arc144/tasks/arc144_b 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $400$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given positive integers a*a* and b*b* such that $a\leq b$, and a sequence of positive integers $A = (A_1, A_2, \ldots, A_N)$. 16 | 17 | On this sequence, you can perform the following operation any number of times (possibly zero): 18 | 19 | - Choose distinct indices $i, j$ ($1\leq i, j \leq N$). Add $a$ to $A_i$ and subtract $b$ from $A_j$. 20 | 21 | Find the maximum possible value of $\min(A_1, A_2, \ldots, A_N)$ after your operations. 22 | 23 | ### Constraints 24 | 25 | - $2\leq N\leq 3\times 10^5$ 26 | - $1\leq a\leq b\leq 10^9$ 27 | - $1\leq A_i\leq 10^{9}$ 28 | 29 | ------ 30 | 31 | ### Input 32 | 33 | Input is given from Standard Input in the following format: 34 | 35 | > $N$ $a$ $b$ 36 | > $A_1$ $A_2$ $\ldots$ $A_N$ 37 | 38 | ### Output 39 | 40 | Print the maximum possible value of $\min(A_1, A_2, \ldots, A_N)$ after your operations. 41 | 42 | ------ 43 | 44 | ### Sample Input 1 45 | 46 | > 3 2 2 47 | > 1 5 9 48 | 49 | ### Sample Output 1 50 | 51 | > 5 52 | 53 | Here is one way to achieve $\min(A_1, A_2, A_3) = 5$. 54 | 55 | - Perform the operation with $i = 1, j = 3$. $A$ becomes $(3, 5, 7)$. 56 | - Perform the operation with $i = 1, j = 3$. $A$ becomes $(5, 5, 5)$. 57 | 58 | ------ 59 | 60 | ### Sample Input 2 61 | 62 | > 3 2 3 63 | > 11 1 2 64 | 65 | ### Sample Output 2 66 | 67 | > 3 68 | 69 | Here is one way to achieve $\min(A_1, A_2, A_3) = 3$. 70 | 71 | - Perform the operation with $i = 1, j = 3$. $A$ becomes $(13, 1, -1)$. 72 | - Perform the operation with $i = 2, j = 1$. $A$ becomes $(10, 3, -1)$. 73 | - Perform the operation with $i = 3, j = 1$. $A$ becomes $(7, 3, 1)$. 74 | - Perform the operation with $i = 3, j = 1$. $A$ becomes $(4, 3, 3)$. 75 | 76 | ------ 77 | 78 | ### Sample Input 3 79 | 80 | > 3 1 100 81 | > 8 5 6 82 | 83 | ### Sample Output 3 84 | 85 | > 5 86 | 87 | You can achieve $\min(A_1, A_2, A_3) = 5$ by not performing the operation at all. 88 | 89 | ------ 90 | 91 | ### Sample Input 4 92 | 93 | > 6 123 321 94 | > 10 100 1000 10000 100000 1000000 95 | 96 | ### Sample Output 4 97 | 98 | > 90688 99 | 100 | ------ 101 | 102 | ### 解题笔记 103 | 104 | 最开始用模拟写了遍,理所当然超时了,然后就不会做了(太久没练又退化了 105 | 106 | 这题是用二分来解决,在 $1\sim 10^9$ 的区间二分找到最大的 $C$,使进行若干次操作后的 $A[i]\geq C\ (0\leq i 122 | #define int long long 123 | 124 | using namespace std; 125 | 126 | const int MAXN = 3e5 + 20; 127 | int N, a, b; 128 | int A[MAXN]; 129 | 130 | bool ok(int n) 131 | { 132 | int x = 0, y = 0; 133 | for (int i = 0; i < N; i++) 134 | { 135 | if (A[i] >= n) 136 | x += (A[i] - n) / b; 137 | else 138 | y += ceil(1.0 * (n - A[i]) / a); 139 | } 140 | return x >= y; 141 | } 142 | 143 | signed main() 144 | { 145 | cin >> N >> a >> b; 146 | for (int i = 0; i < N; i++) 147 | cin >> A[i]; 148 | int l = 0, r = INT32_MAX; 149 | while (l < r) 150 | { 151 | int mid = (l + r) / 2; 152 | if (ok(mid)) 153 | l = mid + 1; 154 | else 155 | r = mid; 156 | } 157 | cout << l - 1 << endl; 158 | return 0; 159 | } 160 | ``` 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /030~039/034【算法】Prim 算法.md: -------------------------------------------------------------------------------- 1 | **求最小生成树(适合稠密图):**Prim (普林姆) 算法 2 | 3 | 4 | 5 | # Prim 算法 6 | 7 | ## 朴素版本 8 | 9 | ### 复杂度 10 | 11 | 时间复杂度:$O(\left|V\right|^2)$ 12 | 13 | $\left|V\right|$ 为顶点数,该版本适合稠密图,即 $\left|E\right|接近\left|V\right|^2$ 的图。 14 | 15 | ### 分析 16 | 17 | 该算法的核心思路是,在能到达的边中选择最短的不会成环的边。如左图,我们从 $1$ 号点开始(可任选起点),目前 $1$ 号点的最短的边是 $1\leftrightarrow3$,因此选择该边。选择后可到达的边增多了,然后再在这些边中找到最短的边是 $3\leftrightarrow6$,则选择该边。后面不断进行该操作直到选择了 $\left|V\right|-1$ 条边。 18 | 19 | | | | 20 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 21 | 22 | 上述思路与 Dijkstra 算法有相似之处,每次迭代均是选择最短的边。因此我们模仿 Dijkestra 的结构实现该算法,伪代码如下: 23 | 24 | ``` 25 | prim (G, dist[]) 26 | 初始化: dist[1~v]=+INF, dist[1]=0, ans=0 27 | for (循环v次) 28 | t=未选择的点的集合中dist[t]最小的顶点 29 | if (t为无穷大) 30 | return 图不连通 31 | ans加上dist[t] 32 | 记t已选择 33 | for (j : 从t出发能到达的顶点的集合) 34 | if (若t到j的距离小于j到已选择点的距离) 35 | 更新dist[j]; 36 | return ans 37 | ``` 38 | 39 | 注意,该算法中 dist 的含义不再是 Dijkstra 中到起点的距离,而是到已选择的点的最短距离,更新 dist[j] 时也直接使用边长即可。 40 | 41 | ### 代码实现 42 | 43 | 下面的代码和 Dijkstra 朴素版非常类似,大家可以对照查看,感受两个算法的异同。 44 | 45 | ```cpp 46 | const int MAXN = 510, INF = 0x3f3f3f3f; // INF代表无穷大 47 | int g[MAXN][MAXN]; // 邻接矩阵存图 48 | int dist[MAXN]; // 距离已选择点的最短距离 49 | int vis[MAXN]; // 点是否选择 50 | int v, e; // v顶点数 e边数 51 | 52 | int prim() 53 | { 54 | int ans = 0; 55 | memset(dist, 0x3f, sizeof(dist)); 56 | dist[1] = 0; 57 | for (int i = 0; i < v; i++) 58 | { 59 | int t = -1; 60 | for (int j = 1; j <= v; j++) 61 | if (!vis[j] && (t == -1 || dist[t] > dist[j])) 62 | t = j; 63 | if (dist[t] == INF) 64 | return INF; 65 | ans += dist[t]; 66 | vis[t] = true; 67 | for (int j = 1; j <= v; j++) 68 | dist[j] = min(dist[j], g[t][j]); 69 | } 70 | return ans; 71 | } 72 | ``` 73 | 74 | ## 堆优化版本 75 | 76 | ### 复杂度 77 | 78 | 时间复杂度:$O((\left|E\right|+\left|V\right|)\log\left|V\right|)$ 79 | 80 | $\left|E\right|$ 为边数,$\left|V\right|$ 为顶点数,该版本适合稀疏图,即 $\left|E\right|\ll\left|V\right|^2$ 的图。 81 | 82 | ### 分析 83 | 84 | 朴素版本中,每次迭代都有一个查找最短边的过程,并且对所有点进行了更新操作,共需要 $2\left|V\right|$ 次循环。 85 | 86 | 针对查找最短边这一操作,二叉堆是一个很好的数据结构。我们建立一个小顶堆,即可在 $O(1)$ 时间内取得最小值。 87 | 88 | 针对更新操作,我们将存图方式改为邻接表,所以我们就可以只更新与该点相连的顶点,效率更高。 89 | 90 | ### 代码实现 91 | 92 | 下面的代码和 Dijkstra 堆优化版本非常类似,大家可以对照查看,感受两个算法的异同。 93 | 94 | ```cpp 95 | typedef pair PII; 96 | const int MAXN = 100010, INF = 0x3f3f3f3f; // INF代表无穷大 97 | int E, V; // E边数 V顶点数 98 | vector edge[MAXN]; // 储存边,二元组为(权值,终点) 99 | priority_queue, greater> pque; // 短的边在堆顶 100 | int dist[MAXN]; // 储存节点到已选择点的最短距离 101 | bool vis[MAXN]; // 是否已选择该点 102 | 103 | int prim() 104 | { 105 | int ans = 0, cnt = 0; 106 | memset(dist, 0x3f, sizeof(dist)); 107 | dist[1] = 0; 108 | pque.push({dist[1], 1}); 109 | while (!pque.empty()) 110 | { 111 | PII cur = pque.top(); 112 | pque.pop(); 113 | if (vis[cur.second]) 114 | continue; 115 | ans += cur.first; 116 | cnt++; 117 | vis[cur.second] = true; 118 | for (auto next : edge[cur.second]) 119 | { 120 | if (dist[next.second] > next.first) 121 | { 122 | dist[next.second] = next.first; 123 | if (!vis[next.second]) 124 | pque.push({dist[next.second], next.second}); 125 | } 126 | } 127 | } 128 | return cnt == V ? ans : INF; 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /040~049/045【题目】K-colinear Line.md: -------------------------------------------------------------------------------- 1 | **UNIQUE VISION Programming Contest 2022(AtCoder Beginner Contest 248)** 2 | 3 | E - K-colinear Line 4 | 5 | https://atcoder.jp/contests/abc248/tasks/abc248_e 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given $N$ points in the coordinate plane. For each $1\leq i\leq N$, the $i$-th point is at the coordinates $(X_i, Y_i)$. 16 | 17 | Find the number of lines in the plane that pass $K$ or more of the $N$ points. 18 | If there are infinitely many such lines, print `Infinity`. 19 | 20 | ### Constraints 21 | 22 | - $1 \leq K \leq N \leq 300$ 23 | - $\lvert X_i \rvert, \lvert Y_i \rvert \leq 10^9$ 24 | - $X_i\neq X_j$ or $Y_i\neq Y_j$, if $i\neq j$. 25 | - All values in input are integers. 26 | 27 | ------ 28 | 29 | ### Input 30 | 31 | Input is given from Standard Input in the following format: 32 | 33 | > $N$ $K$ 34 | > $X_1$ $Y_1$ 35 | > $X_2$ $Y_2$ 36 | > $\vdots$ 37 | > $X_N$ $Y_N$ 38 | 39 | ### Output 40 | 41 | Print the number of lines in the plane that pass $K$ or more of the $N$ points, or `Infinity` if there are infinitely many such lines. 42 | 43 | ------ 44 | 45 | ### Sample Input 1 46 | 47 | > 5 2 48 | > 0 0 49 | > 1 0 50 | > 0 1 51 | > -1 0 52 | > 0 -1 53 | 54 | ### Sample Output 1 55 | 56 | > 6 57 | 58 | The six lines $x=0$, $y=0$, $y=x\pm 1$, and $y=-x\pm 1$ satisfy the requirement. 59 | For example, $x=0$ passes the first, third, and fifth points. 60 | 61 | Thus, $6$ should be printed. 62 | 63 | ------ 64 | 65 | ### Sample Input 2 66 | 67 | > 1 1 68 | > 0 0 69 | 70 | ### Sample Output 2 71 | 72 | > Infinity 73 | 74 | Infinitely many lines pass the origin. 75 | 76 | Thus, `Infinity` should be printed. 77 | 78 | ### 我的笔记 79 | 80 | 由于 $1 \leq N \leq 300$,范围很小,所以完全可以通过遍历来得出答案。 81 | 82 | 首先在 $N$ 个点中遍历选择两个点 $i$、$j$ ,因为两点可以确定一条直线,因此 $i$、$j$ 确定一条直线。然后再遍历选择第三个点 $k$,$i$、$k$ 也可确定一条直线。若这两条直线斜率相同,那么说明 $i$、$j$、$k$ 共线。遍历完后即可得出该线一共过几个点,若 $\geq K$,则符合条件。这个解法有三层循环,时间复杂度 $O(N^3)$。 83 | 84 | 因为 $i,j$ 和 $j,i$ 确定的同一条直线,所以遍历时需要注意保证 $i 90 | 91 | using namespace std; 92 | 93 | int main() 94 | { 95 | ios::sync_with_stdio(false); 96 | cin.tie(0); 97 | const int MAXN = 310; 98 | int N, K; 99 | cin >> N >> K; 100 | int X[MAXN], Y[MAXN]; 101 | for (int i = 0; i < N; i++) 102 | cin >> X[i] >> Y[i]; 103 | if (K == 1) 104 | { 105 | cout << "Infinity" << endl; 106 | } 107 | else 108 | { 109 | bool vis[MAXN][MAXN]{}; 110 | int ans = 0; 111 | for (int i = 0; i < N - 1; i++) 112 | { 113 | for (int j = i + 1; j < N; j++) 114 | { 115 | if (!vis[i][j]) 116 | { 117 | vector parallel; 118 | parallel.push_back(i); 119 | parallel.push_back(j); 120 | for (int k = j + 1; k < N; k++) 121 | { 122 | if (((X[j] - X[i]) * (Y[k] - Y[i])) == ((Y[j] - Y[i]) * (X[k] - X[i]))) 123 | { 124 | parallel.push_back(k); 125 | } 126 | } 127 | for (int k = 0; k < parallel.size() - 1; k++) 128 | { 129 | for (int l = k + 1; l < parallel.size(); l++) 130 | { 131 | vis[parallel[k]][parallel[l]] = true; 132 | } 133 | } 134 | if (parallel.size() >= K) 135 | ans++; 136 | } 137 | } 138 | } 139 | cout << ans << endl; 140 | } 141 | return 0; 142 | } 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /060~069/066【数论】欧拉函数.md: -------------------------------------------------------------------------------- 1 | **欧拉函数:**对正整数 $n$,欧拉函数是小于 $n$ 的正整数中与 $n$ 互质的数的数目,记作 $\varphi(n)$。 2 | 3 | 4 | 5 | 例如,$\varphi(8)=4$,因为 $1,3,5,7$ 均与 $8$ 互质。注意:$\varphi(1)=1$ 6 | 7 | # 欧拉函数的值 8 | 9 | ## 一般方法 10 | 11 | 若 $n$ 有标准分解 $p_1^{k_1}p_2^{k_2}\cdots p_r^{k_r}$,其中 $p_i$ 为互异的质因子,$k_i\geq1$ 为质因子的次数。则欧拉函数的值为: 12 | $$ 13 | \varphi(n)=n(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_r}) 14 | $$ 15 | 需要注意:$\varphi(1)=1$ 16 | 17 | ### 简单证明 18 | 19 | 若要获得 $1\sim n$ 中的质因子个数,可以将 $n$ 减去 $p_1$ 和它的倍数的数量,减去 $p_2$ 和它的倍数的数量 $\cdots\cdots$ 20 | 21 | 写成表达式即为: 22 | $$ 23 | n-\frac{n}{p_1}-\frac{n}{p_2}-\cdots-\frac{n}{p_r} 24 | $$ 25 | 但是可以发现,$p_1,p_2$ 的公倍数、$p_1,p_3$ 的公倍数 $\cdots\cdots$ 被减去了两次,因此需再加上一次。但又发现 $p_1,p_2,p_3$ 的公倍数、$p_1,p_2,p_4$ 的公倍数 $\cdots\cdots$ 多加了一次,得再减去一次,这便是容斥原理的思维。 26 | 27 | 最终表达式为: 28 | $$ 29 | \begin{align}\\ 30 | \varphi(n)=n&-\frac{n}{p_1}-\frac{n}{p_2}-\cdots-\frac{n}{p_r}\\ 31 | &+\frac{n}{p_1p_2}+\frac{n}{p_1p_3}+\cdots+\frac{n}{p_{r-1}p_r}\\ 32 | &-\frac{n}{p_1p_2p_3}-\frac{n}{p_1p_2p_4}-\cdots-\frac{n}{p_{r-2}p_{r-1}p_r}\\ 33 | &\cdots\cdots 34 | \end{align} 35 | $$ 36 | 通过化简即可得到结果式子的形式。 37 | 38 | ### 程序实现 39 | 40 | 该方法仅求得 $n$ 的欧拉函数值 41 | 42 | - 时间复杂度:$O(\sqrt{n})$ 43 | - 空间复杂度:$O(1)$ 44 | 45 | 编程时需要注意的是,$\frac{1}{p_i}$ 会被整除得到 $0$,因此需将 $1-\frac{1}{p_i}$ 变形为 $\frac{p_i-1}{p_i}$。同时,对 $n$ 先除再乘,防止溢出。 46 | 47 | ```cpp 48 | #include 49 | 50 | using namespace std; 51 | 52 | int main() 53 | { 54 | int a; 55 | cin >> a; 56 | int ans = a; 57 | for (int i = 2; i <= a / i; i++) 58 | { 59 | if (!(a % i)) 60 | { 61 | ans = ans / i * (i - 1); 62 | while (!(a % i)) 63 | a /= i; 64 | } 65 | } 66 | if (a > 1) 67 | ans = ans / a * (a - 1); 68 | cout << ans << endl; 69 | return 0; 70 | } 71 | ``` 72 | 73 | ## 线性筛法 74 | 75 | ### 简单证明 76 | 77 | 通过简单改造线性筛,再筛得质数的同时计算出欧拉函数值。线性筛中有三种情况: 78 | 79 | 1. $i$ 为质数 80 | 2. $i$ 能被 $prime[j]$ 整除 81 | 3. $i$ 不能被 $prime[j]$ 整除 82 | 83 | 我们分类讨论: 84 | 85 | 当 $i$ 为质数时,$i$ 与除 $1$ 外互素,因此 $\varphi(i)=i-1$ 86 | 87 | 当 $i$ 能被 $prime[j]$ 整除时,$prime[j]$ 即为 $i$ 的一个质因子,那么 $i\cdot prime[j]$ 的标准分解的质因子和 $i$ 完全一样,那么: 88 | $$ 89 | \begin{align}\\ 90 | \varphi(i)&=(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_r})\cdot i\\ 91 | \varphi(i\cdot prime[j])&=(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_r})\cdot i\cdot prime[j]\\ 92 | \Rightarrow\varphi(i\cdot prime[j])&=\varphi(i)\cdot prime[j]\\ 93 | \end{align} 94 | $$ 95 | 当 $i$ 不能被 $prime[j]$ 整除时,$prime[j]$ 不时 $i$ 的质因子,但是 $i\cdot prime[j]$ 的一个质因子,那么: 96 | $$ 97 | \begin{align}\\ 98 | \varphi(i)&=(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_r})\cdot i\\ 99 | \varphi(i\cdot prime[j])&=(1-\frac{1}{p_1})(1-\frac{1}{p_2})\cdots(1-\frac{1}{p_r})(1-\frac{1}{prime[j]})\cdot i\cdot prime[j]\\ 100 | \Rightarrow\varphi(i\cdot prime[j])&=\varphi(i)\cdot prime[j]\cdot (1-\frac{1}{prime[j]})=\varphi(i)(prime[j]-1)\\ 101 | \end{align} 102 | $$ 103 | 按照上面的转换方式修改线性筛的代码即可。 104 | 105 | ### 程序实现 106 | 107 | 该方法求得 $1\sim n$ 的所有欧拉函数值 108 | 109 | - 时间复杂度:$O(n)$ 110 | - 空间复杂度:$O(n)$ 111 | 112 | ```cpp 113 | const int MAXN = 1e6 + 10; 114 | int prime[MAXN], phi[MAXN], idx; 115 | bool is_prime[MAXN]; 116 | 117 | void init(int n) 118 | { 119 | memset(is_prime, 1, sizeof(is_prime)); 120 | is_prime[0] = is_prime[1] = false; 121 | phi[1] = 1; 122 | for (int i = 2; i <= n; i++) 123 | { 124 | if (is_prime[i]) 125 | { 126 | prime[idx++] = i; 127 | phi[i] = i - 1; 128 | } 129 | for (int j = 0; j < idx && i * prime[j] <= n; j++) 130 | { 131 | is_prime[i * prime[j]] = false; 132 | if (!(i % prime[j])) 133 | { 134 | phi[i * prime[j]] = phi[i] * prime[j]; 135 | break; 136 | } 137 | phi[i * prime[j]] = phi[i] * (prime[j] - 1); 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | -------------------------------------------------------------------------------- /080~089/082【题目】Factorial and Multiple.md: -------------------------------------------------------------------------------- 1 | **Denso Create Programming Contest 2022 Winter(AtCoder Beginner Contest 280)** 2 | 3 | D - Factorial and Multiple 4 | 5 | https://atcoder.jp/contests/abc280/tasks/abc280_d 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $400$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given an integer $K$ greater than or equal to $2$. 16 | Find the minimum positive integer $N$ such that $N!$ is a multiple of $K$. 17 | 18 | Here, $N!$ denotes the factorial of $N$. Under the Constraints of this problem, we can prove that such an $N$ always exists. 19 | 20 | ### Constraints 21 | 22 | - $2\leq K\leq 10^{12}$ 23 | - $K$ is an integer. 24 | 25 | ------ 26 | 27 | ### Input 28 | 29 | The input is given from Standard Input in the following format: 30 | 31 | > K 32 | 33 | ### Output 34 | 35 | Print the minimum positive integer $N$ such that $N!$ is a multiple of $K$. 36 | 37 | ------ 38 | 39 | ### Sample Input 1 40 | 41 | > 30 42 | 43 | ### Sample Output 1 44 | 45 | > 5 46 | 47 | - $1!=1$ 48 | - $2!=2\times 1=2$ 49 | - $3!=3\times 2\times 1=6$ 50 | - $4!=4\times 3\times 2\times 1=24$ 51 | - $5!=5\times 4\times 3\times 2\times 1=120$ 52 | 53 | Therefore, $5$ is the minimum positive integer $N$ such that $N!$ is a multiple of $30$. Thus, $5$ should be printed. 54 | 55 | ------ 56 | 57 | ### Sample Input 2 58 | 59 | > 123456789011 60 | 61 | ### Sample Output 2 62 | 63 | > 123456789011 64 | 65 | ------ 66 | 67 | ### Sample Input 3 68 | 69 | > 280 70 | 71 | ### Sample Output 3 72 | 73 | > 7 74 | 75 | ### 题解 76 | 77 | (这题比赛的时候用不正常的方法混过了,现在补一遍正解) 78 | 79 | 该题要分解质因数,这点应该比较好想,不过之后的处理方式才是重点。即: 80 | 81 | 对于分解好的数 $K=p_0^{a_0}p_1^{a_1}\dots p_n^{a_n}\ (p_0,p_1,\dots,p_n为质数)$,如何求得最小的 $N!$ 满足 $K\mid N!$ 82 | 83 | 我们可以单独考虑每一个质数,对于 $p_i^{a_i}$,取 $N=p_i\cdot a_i$ 一定能满足条件,但是该答案不一定是最小的。具体举例来说: 84 | 85 | 如 $3^4$,取 $N=3\cdot4=12$,$N!=12\dots\times9\times\dots\times6\times\dots\times3\times\dots$,可以看见,$9$ 里面实际上包含了两个 $3$,因此我们没有必要令 $N=12$,只需要 $N=9$ 即可。 86 | 87 | 那么如何求到最小的呢?分解质因数后能保证时间复杂度为 $\log$ 级别,因此我们从直接一个个算就行了。使用变量 $cnt$ 计数,计算 $i\cdot p_i\ (i=1,2,\dots)$ 内含有的 $p_i$ 因子个数 $n$,将 $cnt$ 加上 $n$,直到 $cnt\geq a_i$ 时,$i\cdot p_i$ 便是所求的 $N$。具体举例来说: 88 | 89 | 如 $3^4$,我们需要 $4$ 个 $3$,从 $1\times3$ 开始,$3$ 含有 $1$ 个 $3$,$6$ 含有 $1$ 个 $3$,$9$ 含有 $2$ 个 $3$,此时已经组成了 $4$ 个 $3$,$N=9$ 90 | 91 | 考虑完单个质数之后,整个 $K$ 的答案便是每个质数因子的答案的最大值。 92 | 93 | ### 代码 94 | 95 | ```cpp 96 | #include 97 | #define endl '\n' 98 | 99 | using namespace std; 100 | 101 | typedef long long ll; 102 | const int MAXN = 1e6 + 10; 103 | int prime[MAXN], primesize = 0; 104 | bool not_prime[MAXN]; 105 | void init_prime() 106 | { 107 | not_prime[0] = not_prime[1] = true; 108 | for (int i = 2; i < MAXN; i++) 109 | { 110 | if (!not_prime[i]) 111 | prime[primesize++] = i; 112 | for (int j = 0; j < primesize && i * prime[j] < MAXN; j++) 113 | { 114 | not_prime[i * prime[j]] = true; 115 | if (i % prime[j] == 0) 116 | break; 117 | } 118 | } 119 | } 120 | 121 | int main() 122 | { 123 | ios_base::sync_with_stdio(false); 124 | cin.tie(0); 125 | cout.tie(0); 126 | init_prime(); 127 | ll K; 128 | cin >> K; 129 | ll ans = 0; 130 | for (int i = 0; i < primesize; i++) 131 | { 132 | ll prm = prime[i], cnt = 0; 133 | if (prm * prm > K) 134 | break; 135 | while (!(K % prm)) 136 | { 137 | K /= prm; 138 | cnt++; 139 | } 140 | ll n = 0; 141 | while (cnt > 0) 142 | { 143 | n += prm; 144 | int x = n; 145 | while (!(x % prm)) 146 | { 147 | x /= prm; 148 | cnt--; 149 | } 150 | } 151 | ans = max(ans, n); 152 | } 153 | cout << max(ans, K) << endl; 154 | return 0; 155 | } 156 | ``` 157 | 158 | -------------------------------------------------------------------------------- /050~059/057【算法】高精度运算.md: -------------------------------------------------------------------------------- 1 | **高精度运算:**参与运算的数范围大大,超出了标准数据类型能表示的范围的运算。 2 | 3 | 4 | 5 | 标准数据类型可以表示的整数范围:$\text{int:}[-2^{31},2^{31})$、$\text{long long:}[-2^{63},2^{63})$. 而通过高精度算法,我们可以处理极大的数据运算,例如**长度**为 $10^{6}$ 的数的加减运算。 6 | 7 | # 算法思路 8 | 9 | ### 数字储存 10 | 11 | 普通整型将数字整体二进制储存,而高精度整型将数字的每一位单独储存。即使用一个整型数组,每一位分别储存数字的每一位。 12 | 13 | 例如 $998244353$ 这个数,使用高精度储存即为 $[3,5,3,4,4,2,8,9,9]$. 为了进位方便,我们往往将数字反向储存,即个位放在数组第 $0$ 位,以此类推。当然若没有进位问题,如仅考虑除法,正向储存其实也可以,但不推荐。 14 | 15 | ### 运算操作 16 | 17 | 高精度算法可以看作模拟人**列竖式运算**,可以自己在纸上试一下竖式运算的步骤,即可理解代码的含义。 18 | 19 | 注意:下面的算法没有考虑正负性,若存在符号问题,需要修改使用方式。 20 | 21 | # 高精度整型输入/输出 22 | 23 | ```cpp 24 | /* 变量 */ 25 | string a; 26 | vector A; 27 | 28 | /* 输入 */ 29 | cin >> a; // 首先以字符串形式读入 30 | for (int i = a.size() - 1; i >= 0; i--) 31 | A.push_back(a[i] - '0'); // 反向将字符串每位写入整型数组,注意减去偏移量‘0’ 32 | 33 | /* 输出 */ 34 | for (int i = A.size() - 1; i >= 0; i--) 35 | cout << A[i]; // 反向输出整型数组每一位 36 | ``` 37 | 38 | # 高精度整型 + 高精度整型 39 | 40 | ```cpp 41 | vector add(vector &A, vector &B) 42 | { 43 | if (B.size() > A.size()) 44 | return add(B, A); 45 | vector C; 46 | int t = 0; 47 | for (int i = 0; i < A.size(); i++) 48 | { 49 | t += A[i]; 50 | if (i < B.size()) 51 | t += B[i]; 52 | C.push_back(t % 10); 53 | t = t > 9 ? 1 : 0; 54 | } 55 | if (t) 56 | C.push_back(1); 57 | return C; 58 | } 59 | ``` 60 | 61 | 注意: 62 | 63 | 1. 开头 if 的作用为保证 $A的位数\geq B的位数$,当然可以通过修改下面的循环和判断条件使其适配所有情况。 64 | 2. for 循环中的 if 是为了防止越界访问 $B$ 。 65 | 3. 结尾需要判断 $t$ 是否为 $0$,若非 $0$ 则需要进位 $1$. 66 | 67 | # 高精度整型 - 高精度整型 68 | 69 | ```cpp 70 | vector sub(vector &A, vector &B) 71 | { 72 | vector C; 73 | int t = 0; 74 | for (int i = 0; i < A.size(); i++) 75 | { 76 | t = A[i] - t; 77 | if (i < B.size()) 78 | t -= B[i]; 79 | C.push_back((t + 10) % 10); 80 | t = t < 0 ? 1 : 0; 81 | } 82 | while (C.size() > 1 && C.back() == 0) 83 | C.pop_back(); 84 | return C; 85 | } 86 | ``` 87 | 88 | 注意: 89 | 90 | 1. 改算法需要保证 $A\geq B$,即答案必须为正,因此需要用下文的调用方法。 91 | 2. for 循环中的 if 是为了防止越界访问 $B$ 。 92 | 3. $(t + 10) \% 10$ 配合下一行,简单地实现了借位操作。 93 | 4. 结尾 while 循环功能是去除多余的前导 $0$,即 $000123$ 取出后为 $123$. 94 | 95 | ```cpp 96 | bool cmp(vector &A, vector &B) // 若A>=B返回true,否则返回false 97 | { 98 | if (A.size() != B.size()) 99 | return A.size() > B.size(); 100 | for (int i = A.size() - 1; i >= 0; i--) 101 | if (A[i] != B[i]) 102 | return A[i] > B[i]; 103 | return true; 104 | } 105 | 106 | int main() 107 | { 108 | if (cmp(A, B)) 109 | vector C = sub(A, B); 110 | else 111 | vector C = sub(B, A); // 这种情况输出时记得先输出一个‘-’符号 112 | } 113 | ``` 114 | 115 | # 高精度整型 * 整型 116 | 117 | ```cpp 118 | vector mul(vector &A, int b) 119 | { 120 | vector C; 121 | int t = 0; 122 | for (int i = 0; i < A.size() || t; i++) 123 | { 124 | if (i < A.size()) 125 | t += A[i] * b; 126 | C.push_back(t % 10); 127 | t /= 10; 128 | } 129 | while (C.size() > 1 && C.back() == 0) 130 | C.pop_back(); 131 | return C; 132 | } 133 | ``` 134 | 135 | 注意: 136 | 137 | 1. for 循环判断条件有 || t,是为了处理最后的进位。 138 | 2. for 循环内的 if 防止越界访问 $A$. 139 | 3. 结尾 while 循环功能是去除多余的前导 $0$. 140 | 141 | # 高精度整型 / 整型 142 | 143 | ```cpp 144 | vector div(vector &A, int b, int &r) 145 | { 146 | vector C; 147 | r = 0; 148 | for (int i = A.size() - 1; i >= 0; i--) 149 | { 150 | r = r * 10 + A[i]; 151 | C.push_back(r / b); 152 | r %= b; 153 | } 154 | reverse(C.begin(), C.end()); 155 | while (C.size() > 1 && C.back() == 0) 156 | C.pop_back(); 157 | return C; 158 | } 159 | ``` 160 | 161 | 注意: 162 | 163 | 1. 除法我们也是模拟的竖式除法,因此是从高位像低位计算,因此 for 循环方向与上面三种相反。 164 | 2. reverse 函数在头文件 algorithm 中,为了将反向的答案翻转还原。 165 | 3. 结尾 while 循环功能是去除多余的前导 $0$. 166 | 167 | -------------------------------------------------------------------------------- /001~009/006【数据结构】单调队列、单调栈.md: -------------------------------------------------------------------------------- 1 | **在队列的基础上维护一个单调性的数据结构:**单调队列 (Monotonic Queue) 2 | 3 | **在栈的基础上维护一个单调性的数据结构:**单调栈 (Monotonic Stack) 4 | 5 | 6 | 7 | # 单调队列 8 | 9 | ## 复杂度 10 | 11 | 时间复杂度:$O(n)$ 12 | 13 | 空间复杂度:$O(n)$ 14 | 15 | ($n$ 为计算范围) 16 | 17 | ## 代码实现 18 | 19 | #### STL 队列 20 | 21 | ```cpp 22 | // val[ ]: 储存数据的数组 23 | // n: 需要计算的范围 24 | // k: 给定的区间大小 25 | // q: STL双向队列,储存val[ ]中元素的数组序号 26 | deque q; 27 | for (int i = 0; i < n; i++) 28 | { 29 | // 去尾操作 30 | while (!q.empty() && val[q.back()] > val[i]) 31 | q.pop_back(); 32 | // 新元素(的序号)入队 33 | q.push_back(i); 34 | // 判断是否需要进行下面两个操作 35 | if (i >= k - 1) 36 | { 37 | // 删头操作 38 | if (q.front() < i - k + 1) 39 | q.pop_front(); 40 | // 输出操作 41 | cout << val[q.front()] << ' '; 42 | } 43 | } 44 | ``` 45 | 46 | #### 数组模拟队列 47 | 48 | ```cpp 49 | // val[ ]: 储存数据的数组 50 | // n: 需要计算的范围 51 | // k: 给定的区间大小 52 | // q: 数组队列,储存val[ ]中元素的数组序号 53 | // h, t: 队头、队尾指针,(0, -1)为空 54 | int q[MAXN], h = 0, t = -1; 55 | for (int i = 0; i < n; i++) 56 | { 57 | while (h <= t && val[q[t]] > val[i]) 58 | t--; 59 | q[++t] = i; 60 | if (i >= k - 1) 61 | { 62 | if (q[h] < i - k + 1) 63 | h++; 64 | cout << val[q[h]] << ' '; 65 | } 66 | } 67 | ``` 68 | 69 | ## 分析 70 | 71 | (以单调递减序列为例) 72 | 73 | 流程:去尾操作 -> 入队操作 -> 删头操作 -> 输出操作 -> (循环) 74 | 75 | **去尾操作:** 76 | 77 | a. 若队列为空:直接跳到下一步 78 | 79 | b. 若队列非空,若当前序号对应的数字比队尾小(即符合单调递减):不去尾,跳到下一步 80 | 81 | c. 若队列非空,若当前序号对应的数字比队尾小(不符合单调递减):弹出队尾,再次比较,循环操作直到队尾符合条件或队列空 82 | 83 | **入队操作:** 84 | 85 | 将当前数字对应的序号入队 86 | 87 | (判断是否需要进行下面两个操作:当读取过的数据还没有k个时,不需要删头操作,也不需要输出) 88 | 89 | **删头操作:** 90 | 91 | a. 若队首(也就是队列最大值)还处于当前区间内:则队首对应的数字就是区间最大值,不删头,跳到下一步 92 | 93 | b. 若队首(也就是队列最大值)处于当前区间外:弹出队首,再次比较,循环操作直到队首符合条件,此时队首对应的原数字就是区间最大值 94 | 95 | **输出操作:** 96 | 97 | 输出队首序号对应到原数组中的数字,即当前区间最大值 98 | 99 | ## 例题 100 | 101 | POJ2823 - Sliding Window: http://poj.org/problem?id=2823 102 | 103 | # 单调栈 104 | 105 | ## 复杂度 106 | 107 | 时间复杂度:$O(n)$ 108 | 109 | 空间复杂度:$O(n)$ 110 | 111 | ($n$ 为计算范围) 112 | 113 | ## 代码实现 114 | 115 | #### STL 栈 116 | 117 | ```cpp 118 | // val[ ]: 储存数据的数组 119 | // n: 需要计算的范围 120 | // s: STL栈,储存val[ ]中元素的数组序号 121 | stack s; 122 | for (int i = 0; i < n; i++) 123 | { 124 | // 删除操作 125 | while (!s.empty() && val[s.top()] > val[i]) 126 | s.pop(); 127 | // 输出操作 128 | cout << (s.empty() ? -1 : val[s.top()]) << ' '; 129 | // 新元素(的序号)入队 130 | s.push(i); 131 | } 132 | ``` 133 | 134 | #### 数组模拟栈 135 | 136 | ```cpp 137 | // val[ ]: 储存数据的数组 138 | // n: 需要计算的范围 139 | // s: 数组栈,储存val[ ]中元素的数组序号 140 | // t: 栈顶指针,0为空 141 | int s[MAXN], t; 142 | for (int i = 0; i < n; i++) 143 | { 144 | while (idx && num[s[t]] >= num[i]) 145 | t--; 146 | cout << (!t ? -1 : val[s[t]]) << ' '; 147 | s[++t] = i; 148 | } 149 | ``` 150 | 151 | ## 分析 152 | 153 | 与单调队列几乎一样,就是单调队列去掉了删头操作。 154 | 155 | ## 例题 156 | 157 | POJ3250 - Bad Hair Day: http://poj.org/problem?id=3250 158 | 159 | POJ2559 - Largest Rectangle in a Histogram: http://poj.org/problem?id=2559 160 | 161 | # 二维单调队列 162 | 163 | ## 复杂度 164 | 165 | 时间复杂度:$O(n^2)$ 166 | 167 | 空间复杂度:$O(n^2)$ 168 | 169 | ($n$ 为计算范围) 170 | 171 | ## 分析 172 | 173 | 对于二维情况,我们的思路就是,先用二维压缩为一维,再处理一维情况。核心思想就是,要求一个 $a\times b$ 的矩阵最小值,可以先将每行的最小值求出来,一共 $a$ 个数,然后再求这 $a$ 个数的最小值,得到的便是矩阵中的最小值。 174 | 175 | 以 $5\times5$ 的矩阵为例,我们要在其中求 $3\times3$ 的子矩阵的元素的最小值: 176 | 177 | 178 | 179 | 第一步,我们对每行分别使用单调队列求区间最小值,得到如下结果(灰色代表无意义数据): 180 | 181 | 182 | 183 | 第二步,我们对每列分别使用单调队列求区间最小值,得到以下结果(灰色代表无意义数据): 184 | 185 | 186 | 187 | 此时,第 $i$ 行第 $j$ 列的值就代表着,以 $(i,j)$ 为右下顶点的 $3\times3$ 子矩阵的最小值。 188 | 189 | ## 代码实现 190 | 191 | 与一维单调队列一致,只不过是使用了两次而已。 192 | 193 | ## 例题 194 | 195 | AcWing 1091 - 理想的正方形: https://www.acwing.com/problem/content/description/1093/ 196 | -------------------------------------------------------------------------------- /070~079/073【题目】Permutation Operations.md: -------------------------------------------------------------------------------- 1 | **Codeforces Global Round 23** 2 | 3 | C. Permutation Operations 4 | 5 | https://codeforces.com/contest/1746/problem/C 6 | 7 | 8 | 9 | 2 seconds / 256 megabytes 10 | 11 | standard input / standard output 12 | 13 | ### Problem 14 | 15 | You are given a permutation $a$ of size $n$ and you should perform $n$ operations on it. In the $i$-th operation, you can choose a non-empty suffix of $a$ and increase all of its elements by $i$. How can we perform the operations to minimize the number of inversions in the final array? 16 | 17 | Note that you can perform operations on the same suffix any number of times you want. 18 | 19 | A permutation of size $n$ is an array of size n such that each integer from $1$ to $n$ occurs exactly once in this array. A suffix is several consecutive elements of an array that include the last element of the array. An inversion in an array $a$ is a pair of indices $(i, j)$ such that $i > j$ and $a_{i} < a_{j}$. 20 | 21 | ### Input 22 | 23 | Each test contains multiple test cases. The first line contains the number of test cases $t$ ($1 \le t \le 10^4$). The description of the test cases follows. 24 | 25 | The first line of each test case contains a single integer $n$ ($1 \le n \le 10^5$) — the size of the array. 26 | 27 | The second line contains $n$ distinct integers $a_{1}, a_{2}, \dots, a_{n}$ ($1 \le a_i \le n$), the initial permutation $a$. 28 | 29 | It's guaranteed that the sum of $n$ over all test cases does not exceed $2 \cdot 10^5$. 30 | 31 | ### Output 32 | 33 | For each test case, print $n$ integers $x_{1}, x_{2}, \ldots, x_{n}$ ($1 \le x_{i} \le n$ for each $1 \le i \le n$) indicating that the $i$-th operation must be applied to the suffix **starting at index** $x_{i}$. If there are multiple answers, print any of them. 34 | 35 | ### Example 36 | 37 | input 38 | 39 | > 4 40 | > 4 41 | > 1 2 3 4 42 | > 5 43 | > 1 3 2 4 5 44 | > 3 45 | > 2 3 1 46 | > 1 47 | > 1 48 | 49 | output 50 | 51 | > 1 1 1 1 52 | > 1 4 3 2 1 53 | > 1 3 3 54 | > 1 55 | 56 | ### Note 57 | 58 | In the first test case one of the optimal solutions is to increase the whole array on each operation (that is, choose the suffix starting at index $1$). The final array $[11, 12, 13, 14]$ contains $0$ inversions. 59 | 60 | In the second test case, a will be equal to $[2, 4, 3, 5, 6]$, $[2, 4, 3, 7, 8]$, $[2, 4, 6, 10, 11]$, $[2, 8, 10, 14, 15]$ and $[7, 13, 15, 19, 20]$ after the first, second, third, fourth, and fifth operations, respectively. So the final array $a$ has zero inversions. 61 | 62 | ### 题解 63 | 64 | 首先我们要能猜到,对任意排列进行 $n$ 次该操作,一定有方法得到一个单调不减序列,即逆序对数目一定能变为 $0$. 下面来解释为什么: 65 | 66 | - 对于一个数列 $[a_1,a_2,a_3,a_4,\dots]$,令 $[b_1,b_2,b_3,b_4,\dots]$ 为它的差分数列 $[a_1,a_2-a_1,a_3-a_2,a_4-a_3,\dots]$ 67 | - 原数列单调不减,即意味着它的差分数列非负,即 $b_i\geq0$. 68 | - 将 $a_k,a_{k+1},\dots,a_n$ 加 $i$ 的操作,即将差分数列中的 $b_k$ 的值增加 $k$. 69 | - 由于原数列为一个排列,意味着 $a_i>0$,则 $b_i=a_i-a_{i-1}>-a_{i-1}$. 70 | 71 | 根据以上已知条件,我们可以构造出一种方案,保证操作后,数列为单调不减数列: 72 | 73 | - 对于每个 $a_i$ ($i=1,2,\dots,n-1$),将 $a_{i+1},\dots,a_n$ 加 $a_i$ 74 | 75 | 因为 $b_{i+1}=a_{i+1}-a_{i}>-a_i$,进行上面的操作后,$b_{i+1}+a_i=a_{i+1}>0$,可以满足数列单调不减。同时,由于 $a_i$ 是一个排列,因此 $a_i\neq a_j$ (如果 $i\neq j$) 且 $a_i\in[1,n]$. 我们有 $n$ 次操作,正好一一对应一个 $a_i$. 76 | 77 | 当然,大家也可能注意到,上面括号内有条件 $i=1,2,\dots,n-1$。若 $i=n$ 时,$a_{i+1}$ 不存在。实际上,这种情况的怎么处理不影响最终结果,可以随意输出。下面代码的处理方式为 `i % n + 1`,这种情况将会输出为 $1$,即整个序列一起增加。 78 | 79 | ### 代码 80 | 81 | - 时间复杂度:$O(n)$ 82 | - 空间复杂度:$O(n)$ 83 | 84 | ```cpp 85 | #include 86 | 87 | using namespace std; 88 | 89 | const int MAXN = 1e5 + 10; 90 | int ans[MAXN]; 91 | 92 | void solve() 93 | { 94 | int n; 95 | cin >> n; 96 | for (int i = 1; i <= n; i++) 97 | { 98 | int t; 99 | cin >> t; 100 | ans[t] = i % n + 1; 101 | } 102 | for (int i = 1; i <= n; i++) 103 | cout << ans[i] << ' '; 104 | cout << endl; 105 | } 106 | 107 | int main() 108 | { 109 | int t; 110 | cin >> t; 111 | while (t--) 112 | solve(); 113 | return 0; 114 | } 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /070~079/072【题目】Orray.md: -------------------------------------------------------------------------------- 1 | **Codeforces Round #827 (Div. 4)** 2 | 3 | G. Orray 4 | 5 | https://codeforces.com/contest/1742/problem/G 6 | 7 | 8 | 9 | 2 seconds / 256 megabytes 10 | 11 | standard input / standard output 12 | 13 | ### Problem 14 | 15 | You are given an array $a$ consisting of $n$ nonnegative integers. 16 | 17 | Let's define the prefix OR array $b$ as the array $b_i = a_1~\mathsf{OR}~a_2~\mathsf{OR}~\dots~\mathsf{OR}~a_i$, where $\mathsf{OR}$ represents the [bitwise OR operation](https://en.wikipedia.org/wiki/Bitwise_operation#OR). In other words, the array $b$ is formed by computing the $\mathsf{OR}$ of every prefix of $a$. 18 | 19 | You are asked to rearrange the elements of the array $a$ in such a way that its prefix OR array is lexicographically maximum. 20 | 21 | An array $x$ is lexicographically greater than an array $y$ if in the first position where $x$ and $y$ differ, $x_i > y_i$. 22 | 23 | ### Input 24 | 25 | The first line of the input contains a single integer $1 \le t \le 100$ — the number of test cases. The description of test cases follows. 26 | 27 | The first line of each test case contains a single integer $n$ ($1 \leq n \leq 2 \cdot 10^5$) — the length of the array $a$. 28 | 29 | The second line of each test case contains $n$ **nonnegative** integers $a_1, \ldots, a_n$ ($0 \leq a_i \leq 10^9$). 30 | 31 | It is guaranteed that the sum of $n$ over all test cases does not exceed $2 \cdot 10^5$. 32 | 33 | ### Output 34 | 35 | For each test case print $n$ integers — any rearrangement of the array $a$ that obtains the lexicographically maximum prefix OR array. 36 | 37 | ### Example 38 | 39 | input: 40 | 41 | > 5 42 | > 4 43 | > 1 2 4 8 44 | > 7 45 | > 5 1 2 3 4 5 5 46 | > 2 47 | > 1 101 48 | > 6 49 | > 2 3 4 2 3 4 50 | > 8 51 | > 1 4 2 3 4 5 7 1 52 | 53 | output: 54 | 55 | > 8 4 2 1 56 | > 5 2 1 3 4 5 5 57 | > 101 1 58 | > 4 3 2 2 3 4 59 | > 7 1 4 2 3 4 5 1 60 | 61 | ### 题解 62 | 63 | 先考虑暴力做法,每一次取数遍历整个数列,找到与前缀或的上一位做 $\mathsf{OR}$ 操作的结果最大的数,从原序列中删除并加入答案序列(代码实现为直接输出该数并打上标记),代码如下: 64 | 65 | ```cpp 66 | int pre = 0, tmp = 0; 67 | for (int i = 0; i < n; i++) 68 | { 69 | int idx = -1; 70 | for (int j = 0; j < n; j++) 71 | { 72 | if (!vis[j] && tmp < (pre | a[j])) 73 | { 74 | tmp = (pre | a[j]); 75 | idx = j; 76 | } 77 | } 78 | pre = tmp; 79 | cout << a[idx] << ' '; 80 | vis[idx] = true; 81 | } 82 | ``` 83 | 84 | 用这个代码测样例就会发现问题:找到并输出了几个数之后,得到了前几个数的前缀或,再和数列中剩下的数取或得到的结果都是一样的,即 $idx$ 一直为初值 $-1$,作为数组下标然后产生异常。 85 | 86 | 为了解决这个问题,我们当然可以把第 $7$ 行的 $<$ 改成 $\leq$,然后喜提一个 $O(n^2)$ 的 $\text{TLE}$ 代码。但同时这也是个突破口,提示着正确的解题方式。 87 | 88 | 注意整数 $a_i$ 最大为 $10^9$,那么 $a_i$ 的二进制表示最多有 $\lfloor\log_2{10^9}\rfloor+1=30$ 位,那么意味着,我们最多找到 $30$ 个顺序对结果有影响的数,其他的数的顺序不影响结果。因为最多通过 $30$ 次或运算,我们就能把前缀或中能变成 $1$ 的位都变成 $1$,后面的数已经没有更多的 $1$ 位了。 89 | 90 | 那我们在一次迭代中,如果发现 $idx$ 没有更新,就可以停止迭代,然后按随意顺序输出剩下的数,即可得到答案了。这种方式的复杂度为 $O(n)$,完全可以接受。 91 | 92 | ### 代码 93 | 94 | 易错点:$|$ 运算符的优先级比 $<$,$>$ 低,因此`tmp < (pre | a[j])`的括号一定不要忘了,否则会产生错误结果。 95 | 96 | ```cpp 97 | #include 98 | 99 | using namespace std; 100 | 101 | void solve() 102 | { 103 | int n; 104 | cin >> n; 105 | vector a(n); 106 | vector vis(n); 107 | for (int i = 0; i < n; i++) 108 | cin >> a[i]; 109 | int pre = 0, tmp = 0; 110 | for (int i = 0; i < n; i++) 111 | { 112 | int idx = -1; 113 | for (int j = 0; j < n; j++) 114 | { 115 | if (!vis[j] && tmp < (pre | a[j])) 116 | { 117 | tmp = (pre | a[j]); 118 | idx = j; 119 | } 120 | } 121 | if (idx == -1) 122 | break; 123 | pre = tmp; 124 | cout << a[idx] << ' '; 125 | vis[idx] = true; 126 | } 127 | for (int i = 0; i < n; i++) 128 | if (!vis[i]) 129 | cout << a[i] << ' '; 130 | cout << endl; 131 | } 132 | 133 | int main() 134 | { 135 | int t; 136 | cin >> t; 137 | while (t--) 138 | solve(); 139 | return 0; 140 | } 141 | ``` -------------------------------------------------------------------------------- /160~169/164【数据结构】归并树.md: -------------------------------------------------------------------------------- 1 | **归并树 (Merge Sort Tree):** 归并树是线段树和归并排序的合成,它利用线段树将归并排序的每一步都记录下来。 2 | 3 | - 查找区间 $[l,r]$ 内的大小范围在 $[a,b]$ 的数的个数(类似条件均可查找) 4 | - 查找区间 $[l,r]$ 内第 $k$ 大的数 5 | 6 | 7 | 8 | 归并树的思想基于线段树,因此需要先学习线段树:https://io.zouht.com/117.html 9 | 10 | # 思路 11 | 12 | 归并排序我们不陌生,核心思想就是用递归完成拆分和合并。观察归并的拆分方式,我们可以发现,它和线段树完全一样。 13 | 14 | 下图是线段树的分段示意图,如果我们把它看作归并排序的拆分过程,可以发现其实是一模一样的。 15 | 16 | 17 | 18 | 线段树中,每个节点储存的是一个**数值**,这个数值维护着这一段的信息。 19 | 20 | 而归并树中,每个节点储存的是一个**有序数列**,这个数列就是归并到该节点时的有序数列状态。其实就是利用线段树将归并排序的每一步都记录下来。 21 | 22 | # 应用 23 | 24 | #### 查找区间 $[l,r]$ 内的 $\leq x$ 的数的个数 25 | 26 | 思想和线段树的区间查询完全一致: 27 | 28 | 由长分解到短,对于长度为 $n$ 的数列,初始时考察区间为全体:$s=1,t=n$. 29 | 30 | - 如果线段树内的区间 $[s,t]$ 完全被 $[l,r]$ 包含,在该节点的有序数列中二分找到 $\leq x$ 的数的个数,加入答案。 31 | - 否则,则考察它的左右子区间,令分界点 $m=\lfloor\frac{s+t}{2}\rfloor$: 32 | - 如果 $[s,t]$ 的左子区间 $[s,m]$ 与 $[l,r]$ 有交集($l\leq m$) 33 | - 递归考察 $[s,m]$ 区间 34 | - 如果 $[s,t]$ 的右子区间 $[m+1,t]$ 与 $[l,r]$ 有交集($m+1\leq r$) 35 | - 递归考察 $[m+1,t]$ 区间 36 | - 如果上面的情况都不满足,则说明 $[s,t]$ 区间与我们要求的 $[l,r]$ 区间完全不相交,直接跳过。 37 | 38 | 对于找 $x$ / $\geq x$ 的数的个数,调整符号即可。对于找 $\in[a,b]$ 的数的个数,将 $\leq b$ 的个数减去 $> tree; 66 | 67 | // arr: ori arr, [s, t]: cur seg, x: cur node 68 | void build(const vector &arr, int s, int t, int x) 69 | { 70 | if (s == t) 71 | { 72 | tree[x] = {arr[s]}; 73 | return; 74 | } 75 | int m = (s + t) / 2; 76 | build(arr, s, m, 2 * x); 77 | build(arr, m + 1, t, 2 * x + 1); 78 | merge(tree[2 * x].begin(), tree[2 * x].end(), 79 | tree[2 * x + 1].begin(), tree[2 * x + 1].end(), 80 | back_inserter(tree[x])); 81 | } 82 | 83 | MergeSortTree(const vector &arr) : n(arr.size()) 84 | { 85 | int sz = 1 << (__lg(n) + bool(__builtin_popcount(n) - 1)); // sz = \lceil \log_{2}{n} \rceil 86 | tree.resize(2 * sz); 87 | build(arr, 1, n, 1); 88 | } 89 | 90 | // [l, r]: query array interval, [mn, mx]: query value interval, [s, t]: cur seg, x: cur node 91 | int count(int l, int r, int mn, int mx, int s, int t, int x) 92 | { 93 | if (l <= s && t <= r) 94 | return upper_bound(tree[x].begin(), tree[x].end(), mx) - lower_bound(tree[x].begin(), tree[x].end(), mn); 95 | int m = (s + t) / 2, ans = 0; 96 | if (l <= m) 97 | ans += count(l, r, mn, mx, s, m, x * 2); 98 | if (r > m) 99 | ans += count(l, r, mn, mx, m + 1, t, x * 2 + 1); 100 | return ans; 101 | } 102 | 103 | // query number of elements in the [l, r] interval that fall within the range [mn, mx] 104 | int count(int l, int r, int mn, int mx) 105 | { 106 | return count(l, r, mn, mx, 1, n, 1); 107 | } 108 | 109 | // find the kth smallest number in the [l, r] interval 110 | int count(int l, int r, int k) 111 | { 112 | int pl = 1, pr = n; 113 | while (pl < pr) 114 | { 115 | int mid = (pl + pr) / 2; 116 | if (count(l, r, INT32_MIN, tree[1][mid]) < k) 117 | pl = mid + 1; 118 | else 119 | pr = mid; 120 | } 121 | return tree[1][pl]; 122 | } 123 | }; 124 | ``` 125 | 126 | # 模板题 127 | 128 | Luogu P3834: https://www.luogu.com.cn/problem/P3834 129 | 130 | (该题应当用复杂度更优的可持久化线段树来完成,用本文的归并树应当只能 AC 五个点) -------------------------------------------------------------------------------- /030~039/036【题目】(∀x∀) .md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 242** 2 | 3 | E - (∀x∀) 4 | 5 | https://atcoder.jp/contests/abc242/tasks/abc242_e 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ### Problem Statement 14 | 15 | Solve the following problem for $T$ test cases. 16 | 17 | Given an integer $N$ and a string $S$, find the number of strings $X$ that satisfy all of the conditions below, modulo $998244353$. 18 | 19 | - $X$ is a string of length $N$ consisting of uppercase English letters. 20 | - $X$ is a palindrome. 21 | - $X \le S$ in lexicographical order. 22 | - That is, $X=S$ or $X$ is lexicographically smaller than $S$. 23 | 24 | ### Constraints 25 | 26 | - $1 \le T \le 250000$ 27 | - $N$ is an integer between $1$ and $10^6$ (inclusive). 28 | - In a single input, the sum of $N$ over the test cases is at most $10^6$. 29 | - $S$ is a string of length $N$ consisting of uppercase English letters. 30 | 31 | ------ 32 | 33 | ### Input 34 | 35 | Input is given from Standard Input in the following format: 36 | 37 | > $T$ 38 | > $\mathrm{case}_1$ 39 | > $\mathrm{case}_2$ 40 | > $\vdots$ 41 | > $\mathrm{case}_T$ 42 | 43 | Here, $\mathrm{case}_i$ represents the $i$-th test case. 44 | 45 | Each test case is in the following format: 46 | 47 | > $N$ 48 | > $S$ 49 | 50 | ### Output 51 | 52 | Print $T$ lines. The $i$-th line should contain the answer for the $i$-th test case as an integer. 53 | 54 | ------ 55 | 56 | ### Sample Input 1 57 | 58 | > 5 59 | > 3 60 | > AXA 61 | > 6 62 | > ABCZAZ 63 | > 30 64 | > QWERTYUIOPASDFGHJKLZXCVBNMQWER 65 | > 28 66 | > JVIISNEOXHSNEAAENSHXOENSIIVJ 67 | > 31 68 | > KVOHEEMSOZZASHENDIGOJRTJVMVSDWW 69 | 70 | ### Sample Output 1 71 | 72 | > 24 73 | > 29 74 | > 212370247 75 | > 36523399 76 | > 231364016 77 | 78 | This input contains five test cases. 79 | 80 | Test case #1: 81 | The $24$ strings satisfying the conditions are `AAA`$,$ `ABA`$,$ `ACA`$,...,$ `AXA`. 82 | 83 | Test case #2: 84 | $S$ may not be a palindrome. 85 | 86 | Test case #3: 87 | Be sure to find the count modulo $998244353$. 88 | 89 | ### 我的笔记 90 | 91 | - 若回文串 $X$ 前半截的字典序就已经比 $S$ 小了,那么可以直接得出结论 $XS$,即不符合该题要求。 96 | - 如:$X=ABCBA$ 和 $S=AAAAA$,回文串前半截 $ABC$ 已经比 $AAA$ 大了,因此 $X>S$。 97 | 98 | 依靠上面第一条性质,可以先求出 $X$ 的前半截(就干脆写成 $X/2$ 吧)就已经比 $S/2$ 小的情况。 99 | 100 | 例如字符串 $S=BCADBABDDC$,先只考虑前半截 $S/2=BCADB$。我们要计算有多少种 $X/2$ 比 $S/2$ 小的情况: 101 | 102 | $BCAD?$:第五位可选 $A$,共 $1\times26^0$ 种。 103 | $BCA??$:第四位可选 $A,B,C$,第五位可选 $A\sim Z$,共 $3\times26^1$ 种。 104 | $BC???$:第三位没法选,第四位可选 $A\sim Z$,第五位可选 $A\sim Z$,共 $0\times26^2$ 种 105 | $B????$:第二位可选 $A,B$,第三位可选 $A\sim Z$,第四位可选 $A\sim Z$,第五位可选 $A\sim Z$,共 $2\times26^3$ 种。 106 | $?????$:第一位可选 $A$,第二位可选 $A\sim Z$,第三位可选 $A\sim Z$,第四位可选 $A\sim Z$,第五位可选 $A\sim Z$,共 $1\times26^4$ 种。 107 | 108 | 将这些情况数目加起来,便是 $X/2S$ 不满足题目要求。如果满足题目要求,答案 $+1$即可。 111 | 112 | ### 代码 113 | 114 | ```cpp 115 | #include 116 | #define MOD 998244353 117 | 118 | using namespace std; 119 | 120 | int main(void) 121 | { 122 | int T; 123 | cin >> T; 124 | while (T--) 125 | { 126 | int N; 127 | string S; 128 | cin >> N >> S; 129 | long long ans = 0, p = 1; 130 | for (int i = (N - 1) / 2; i >= 0; i--) 131 | { 132 | ans += p * (S[i] - 'A'); 133 | ans %= MOD; 134 | p *= 26; 135 | p %= MOD; 136 | } 137 | string palindrome = S; 138 | for (int i = 0, j = N - 1; i <= j; i++, j--) 139 | palindrome[j] = palindrome[i]; 140 | if (palindrome <= S) 141 | { 142 | ans++; 143 | ans %= MOD; 144 | } 145 | cout << ans << endl; 146 | } 147 | return 0; 148 | } 149 | ``` 150 | 151 | -------------------------------------------------------------------------------- /090~099/099【题目】Geometric Progression.md: -------------------------------------------------------------------------------- 1 | **AtCoder Beginner Contest 293** 2 | 3 | E - Geometric Progression 4 | 5 | https://atcoder.jp/contests/abc293/tasks/abc293_e 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $500$ points 12 | 13 | ## Problem Statement 14 | 15 | Given integers $A$, $X$, and $M$, find $\displaystyle \sum_{i = 0}^{X-1} A^i$, modulo $M$. 16 | 17 | ## Constraints 18 | 19 | - $1 \leq A, M \leq 10^9$ 20 | - $1 \leq X \leq 10^{12}$ 21 | - All values in the input are integers. 22 | ## Input 23 | 24 | The input is given from Standard Input in the following format: 25 | 26 | > $A$ $X$ $M$ 27 | 28 | ## Output 29 | 30 | Print the answer. 31 | 32 | ## Sample Input 1 33 | 34 | > 3 4 7 35 | 36 | ## Sample Output 1 37 | 38 | > 5 39 | 40 | $3^0 + 3^1 + 3^2 + 3^3 = 40$, which equals $5$ modulo $7$, so $5$ should be printed. 41 | 42 | ## Sample Input 2 43 | 44 | > 8 10 9 45 | 46 | ## Sample Output 2 47 | 48 | > 0 49 | 50 | ## Sample Input 3 51 | 52 | > 1000000000 1000000000000 998244353 53 | 54 | ## Sample Output 3 55 | 56 | > 919667211 57 | 58 | ## 题解 59 | 60 | #### 典型错解——使用等比数列求和公式 61 | 62 | 我们知道等比数列求和公式: 63 | $$ 64 | S_n=a_1\cdot\frac{q^n-1}{q-1} 65 | $$ 66 | 那么易得: 67 | $$ 68 | \displaystyle \sum_{i = 0}^{X-1} A^i=\frac{A^X-1}{A-1} 69 | $$ 70 | 其中, $A^X$ 通过快速幂得到,与 $A-1$ 做除法,在取模的时候转化为乘 $A-1$ 的逆元,然后就便是典型错解。错误的原因是:并不是所有的数都存在乘法逆元。 71 | 72 | 要求 $a$ 对于模 $p$ 的乘法逆元 $b$,即 $a\cdot b\equiv1\pmod p$: 73 | 74 | - 当 $a$ 与 $p$ 互质 75 | - 可用扩展欧几里得算法得出:$\text{exgcd}(a, p, b, t)=1$ 76 | - 特别的,当 $p$ 为质数,可用费马小定理得出:$a^{p-2}$ 77 | - 当 $a$ 与 $p$ 不互质 78 | - $a$ 对于模 $p$ **不存在**乘法逆元 79 | 80 | 由题目可知,没有对 $A$ 和 $M$ 的关系做限制,因此 $A-1$ 可能没有关于 $M$ 的乘法逆元。 81 | 82 | #### 正确解法——矩阵加速递推 83 | 84 | **构造递推** 85 | 86 | 令 $a_n=\displaystyle \sum_{i = 0}^{n-1} A^i$,那么可构造递推式: 87 | $$ 88 | a_{n+1}=Aa_n+1,\ 其中\ a_0=0 89 | $$ 90 | 若直接进行递推,时间复杂度 $O(n)$,由 $1 \leq X \leq 10^{12}$ 可知无法满足时限。 91 | 92 | **加速递推** 93 | 94 | 将上述递推式改写为矩阵形式: 95 | $$ 96 | \begin{bmatrix} 97 | a_{n+1}\\ 98 | 1 99 | \end{bmatrix} 100 | = 101 | \begin{bmatrix} 102 | A & 1\\ 103 | 0 & 1 104 | \end{bmatrix} 105 | \begin{bmatrix} 106 | a_{n}\\ 107 | 1 108 | \end{bmatrix} 109 | ,\ 其中\ a_0=0 110 | $$ 111 | 那么我们的递推式就能变成矩阵次幂形式了: 112 | $$ 113 | \begin{bmatrix} 114 | a_{n}\\ 115 | 1 116 | \end{bmatrix} 117 | = 118 | \begin{bmatrix} 119 | A & 1\\ 120 | 0 & 1 121 | \end{bmatrix}^n 122 | \begin{bmatrix} 123 | 0\\ 124 | 1 125 | \end{bmatrix} 126 | $$ 127 | 同时,矩阵的次幂也是可以使用快速幂的,因为矩阵乘法也满足 $M^{2n}=(M^2)^n$,那么我们直接用快速幂计算出: 128 | $$ 129 | \begin{bmatrix} 130 | A & 1\\ 131 | 0 & 1 132 | \end{bmatrix}^X 133 | = 134 | \begin{bmatrix} 135 | p & q\\ 136 | m & n 137 | \end{bmatrix} 138 | $$ 139 | 题目的答案便是 $q$. 140 | 141 | ## 代码 142 | 143 | ```cpp 144 | #include 145 | 146 | using namespace std; 147 | 148 | typedef long long ll; 149 | 150 | vector> mat_mul_2d(vector> a, vector> b, ll p) 151 | { 152 | vector> ans(2, vector(2)); 153 | ans[0][0] = ((a[0][0] * b[0][0]) % p + (a[0][1] * b[1][0]) % p) % p; 154 | ans[0][1] = ((a[0][0] * b[0][1]) % p + (a[0][1] * b[1][1]) % p) % p; 155 | ans[1][0] = ((a[1][0] * b[0][0]) % p + (a[1][1] * b[1][0]) % p) % p; 156 | ans[1][1] = ((a[1][0] * b[0][1]) % p + (a[1][1] * b[1][1]) % p) % p; 157 | return ans; 158 | } 159 | 160 | vector> mat_fastpow_2d(vector> a, ll n, ll p) 161 | { 162 | vector> ans(2, vector(2)); 163 | ans[0][0] = ans[1][1] = 1; 164 | while (n) 165 | { 166 | if (n % 2) 167 | ans = mat_mul_2d(ans, a, p); 168 | a = mat_mul_2d(a, a, p); 169 | n >>= 1; 170 | } 171 | return ans; 172 | } 173 | 174 | void solve() 175 | { 176 | ll A, X, M; 177 | cin >> A >> X >> M; 178 | vector> mat = {{A, 1}, {0, 1}}; 179 | vector> ans = mat_fastpow_2d(mat, X, M); 180 | cout << ans[0][1] << endl; 181 | } 182 | 183 | signed main() 184 | { 185 | ios::sync_with_stdio(false); 186 | cin.tie(0); 187 | cout.tie(0); 188 | solve(); 189 | return 0; 190 | } 191 | ``` 192 | 193 | -------------------------------------------------------------------------------- /080~089/086【题目】Hossam and Friends.md: -------------------------------------------------------------------------------- 1 | **Codeforces Round #837 (Div. 2)** 2 | 3 | B. Hossam and Friends 4 | 5 | https://codeforces.com/contest/1771/problem/B 6 | 7 | 8 | 9 | 2 seconds / 256 megabytes 10 | 11 | standard input / standard output 12 | 13 | ### Problem 14 | 15 | Hossam makes a big party, and he will invite his friends to the party. 16 | 17 | He has $n$ friends numbered from $1$ to $n$. They will be arranged in a queue as follows: $1, 2, 3, \ldots, n$. 18 | 19 | Hossam has a list of $m$ pairs of his friends that don't know each other. Any pair not present in this list are friends. 20 | 21 | A subsegment of the queue starting from the friend $a$ and ending at the friend $b$ is $[a, a + 1, a + 2, \ldots, b]$. A subsegment of the queue is called good when all pairs of that segment are friends. 22 | 23 | Hossam wants to know how many pairs $(a, b)$ there are ($1 \le a \le b \le n$), such that the subsegment starting from the friend $a$ and ending at the friend $b$ is good. 24 | 25 | ### Input 26 | 27 | The input consists of multiple test cases. The first line contains a single integer $t$ ($1 \le t \le 2 \cdot 10^4$), the number of test cases. Description of the test cases follows. 28 | 29 | The first line of each test case contains two integer numbers $n$ and $m$ ($2 \le n \le 10^5$, $0 \le m \le 10^5$) representing the number of friends and the number of pairs, respectively. 30 | 31 | The $i$-th of the next $m$ lines contains two integers $x_i$ and $y_i$ ($1 \le x_i, y_i\le n$, $x_i \neq y_i$) representing a pair of Hossam's friends that don't know each other. 32 | 33 | Note that pairs can be repeated. 34 | 35 | It is guaranteed that the sum of $n$ over all test cases does not exceed $10^5$, and the sum of $m$ over all test cases does not exceed $10^5$. 36 | 37 | ### Output 38 | 39 | For each test case print an integer — the number of good subsegments. 40 | 41 | ### Example 42 | 43 | Input 44 | 45 | > 2 46 | > 3 2 47 | > 1 3 48 | > 2 3 49 | > 4 2 50 | > 1 2 51 | > 2 3 52 | 53 | Output 54 | 55 | > 4 56 | > 5 57 | 58 | ### Note 59 | 60 | In the first example, the answer is $4$. 61 | 62 | The good subsegments are: 63 | 64 | [1] 65 | 66 | [2] 67 | 68 | [3] 69 | 70 | [1, 2] 71 | 72 | In the second example, the answer is $5$. 73 | 74 | The good subsegments are: 75 | 76 | [1] 77 | 78 | [2] 79 | 80 | [3] 81 | 82 | [4] 83 | 84 | [3, 4] 85 | 86 | ### 题解 87 | 88 | #### 记录方式 89 | 90 | 对于一对不认识的人 $(a,b)$,我们的子序列是不可以同时囊括他们两个的,最多能包含他们其中一个,因此我们可以记录每个人他不认识的人的位置。 91 | 92 | 我们记录的方式为每一对中编号大的指向编号小的,如下图蓝色箭头表示的为 $(3,6),(5,8)$。 93 | 94 | 95 | 96 | 然后我们来考虑子序列是否合法,如上图橙色箭头。子序列 $[6,9]$ 是合法的,因为其只包含了每一对不认识的人中的一个人,但子序列 $[4,9]$ 是不合法的,因为其包含了 $(5,8)$ 这一对不认识的人。 97 | 98 | #### 计算方式 99 | 100 | 我们知道了记录方式,那么如何快速地进行计算呢?我们首先把上图的箭头改为数组的记录方式,如下面的第一行。然后再从左到右取最大值,如下面的第二行: 101 | 102 | 103 | 104 | 这样对于每一个元素,它的下标作为右端点,第二行的数据就是子序列的左端点不能越过的下标。如 $8$ 作为右端点,对应的数据为 $5$,就意味着左端点不可 $\leq5$,那么左端点能为 $6,7,8$ 共三种。 105 | 106 | 这样我们遍历一遍数组,就能得到答案为: 107 | $$ 108 | ans=\sum_{i=1}^{n}{i-a_i} 109 | $$ 110 | 111 | ### 代码 112 | 113 | ```cpp 114 | #include 115 | #define int long long 116 | #define endl '\n' 117 | 118 | using namespace std; 119 | 120 | const int MAXN = 1e5 + 10; 121 | int n, m; 122 | int a[MAXN]; 123 | 124 | void solve() 125 | { 126 | memset(a, 0, sizeof(a)); 127 | cin >> n >> m; 128 | for (int i = 1; i <= m; i++) 129 | { 130 | int x, y; 131 | cin >> x >> y; 132 | if (x < y) 133 | swap(x, y); 134 | a[x] = max(a[x], y); 135 | } 136 | for (int i = 2; i <= n; i++) 137 | a[i] = max(a[i], a[i - 1]); 138 | int ans = 0; 139 | for (int i = 1; i <= n; i++) 140 | ans += i - a[i]; 141 | cout << ans << endl; 142 | } 143 | 144 | signed main() 145 | { 146 | ios_base::sync_with_stdio(false); 147 | cin.tie(0); 148 | cout.tie(0); 149 | int t; 150 | cin >> t; 151 | while (t--) 152 | solve(); 153 | return 0; 154 | } 155 | ``` 156 | 157 | -------------------------------------------------------------------------------- /170~179/175【机器学习】卷积.md: -------------------------------------------------------------------------------- 1 | **卷积** (Convolution): 一种结合两个函数形成新函数的方法,即 $h(x)=f(x)*g(x)$. 2 | 3 | 4 | 5 | # 1 定义与计算 6 | 7 | 如果我们有两个函数 $f(x)$ 和 $g(x)$,如果我们要将他们组合在一起形成 $h(x)$,有很多种方案。例如: 8 | 9 | - 使用加法 $h(x)=f(x)+g(x)$ 10 | - 使用乘法 $h(x)=f(x)\cdot g(x)$ 11 | 12 | 卷积其实也是一种结合方式,使用卷积也可以将两个函数结合在一起:$h(x)=f(x)*g(x)$ 13 | 14 | > 注:卷积运算常用 $*$ 来表示,请注意它不是乘法。 15 | 16 | 到这里你可能会莫名其妙,将两个函数组合起来有什么用?加起来、乘起来或者卷积起来这完全没意义啊。 17 | 18 | 需要注意的是,上面所说的只是**组合方式**,其具体代表的含义得看具体情况。例如两个函数值代表物品数量,我们要求物品数量的和,那么加法组合就具有了意义。在讨论卷积的意义之前,我们必须先知道卷积是怎么计算的。 19 | 20 | 对于连续一维卷积: 21 | $$ 22 | (f*g)(x)=\int _{-\infty }^{\infty }f(\tau )g(x-\tau )\,d\tau 23 | $$ 24 | 对于离散一维卷积: 25 | $$ 26 | (f*g)[x]=\sum _{\tau =-\infty }^{\infty }f[\tau]g[x-\tau] 27 | $$ 28 | 对于离散二维卷积: 29 | $$ 30 | (f*g)[x][y]=\sum _{i =-\infty }^{\infty }\sum _{j =-\infty }^{\infty }f[i][j]g[x-i][x-j] 31 | $$ 32 | 上面这些公式有点抽象,只是为了严谨而放出,用具象的例子更好理解。 33 | 34 | 在计算机科学中,通常讨论的都是离散卷积。在计算机视觉中,通常使用的都是二维卷积。因此用一个二维离散卷积的计算过程举例: 35 | 36 | | 原函数 $f[x][y]$ | 卷积核 $g[x][y]$ | 37 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 38 | | | | 39 | 40 | 第一步,旋转卷积核 $180^{\circ}$: 41 | 42 | 43 | 44 | 第二步,将旋转后的卷积核**盖在**原函数上,例如覆盖在左上角: 45 | 46 | 47 | 48 | 第三步,将原函数和卷积核**对应**位置**相乘**,得到结果在**求和**: 49 | 50 | 即:$1\times9+1\times8+4\times7+1\times6+2\times5+3\times4+1\times3+1\times2+2\times1=80$,得到的结果便是卷积结果对应位置的值。 51 | 52 | 53 | 54 | 这只是执行了卷积的其中一步,记住卷积需要对整个原函数进行操作,因此我们将卷积核在原函数上滑动,计算出最终完整的结果: 55 | 56 | 57 | 58 | 上面便是原函数 $f[x][y]$ 与卷积核 $g[x][y]$ 卷积得到的最终结果了。 59 | 60 | 但是很快我们会发现一个问题,这结果怎么比原函数少了一圈?究其原因是因为原函数并不是像我们公式所写一样是**无限**的二维平面,而只是有限的一小块。这就代表一定会产生边界无法进行卷积,因为卷积核在边界会越界。 61 | 62 | 解决这个问题很简单,扩大一圈边界就行,常见方法有补 $0$ 和对称,把原函数补一圈再卷积就行了: 63 | 64 | | 补 $0$ | 对称 | 65 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 66 | | | | 67 | 68 | # 2 意义 69 | 70 | 就像加法乘法意义,脱离了具体情形,卷积是没有意义的,我们不能直接说卷积一定是具有什么功能的。 71 | 72 | > 下面讨论的均为二维离散卷积 73 | 74 | 决定卷积操作意义的是**卷积核**,卷积核的具体情形决定了卷积操作的功能。 75 | 76 | 例如下面这个卷积核,它的意义便是对原函数取平均: 77 | 78 | 79 | 80 | 又例如下面这个卷积核,它的意义也是平均,不过它并不是上面展示的均匀平均,而是进行高斯平均: 81 | 82 | 83 | 84 | 除了平均,卷积核还能求导: 85 | 86 | | 水平求导 | 竖直求导 | 87 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 88 | | $\begin{bmatrix}-1 & 0 & 1 \\-2 & 0 & 2 \\-1 & 0 & 1\end{bmatrix}$ | $\begin{bmatrix}-1 & -2 & -1 \\0 & 0 & 0 \\1 & 2 & 1\end{bmatrix}$ | 89 | 90 | > 中间是 $2$ 只是一个优化,如果不能理解,就把它当作 $1$ 来看。 91 | 92 | > 上面均讨论的二维离散卷积,3b1b 的视频分享了一个很有趣的一维离散卷积的一种现实中的意义,感兴趣可以观看:https://www.bilibili.com/video/BV1Vd4y1e7pj 93 | 94 | 通过构造**不同**的卷积核,卷积操作可以被赋予**不同意义**,实现**不同功能**。例如是可以通过构造一个卷积核来实现提取图片某个特征的。 95 | 96 | 因此,我们只需要了解卷积是一种运算,一种结合两个函数形成新函数的方法就行了。具体卷积核实现的不同功能,就得在实践中积累学习了。 97 | 98 | # 3 卷积和互相关 99 | 100 | 大家可能已经感觉到,计算中途将卷积核翻转 $180^{\circ}$ 的操作莫名其妙,完全没理由,其实它确实没理由,因为卷积就是这样定义的。不翻转卷积核进行上述运算,那么这个操作在数学中被定义为**互相关** (Correlation). 101 | 102 | 我们没有必要纠结为啥这样定义,这就像为什么加法是这样,乘法是这样一样,这就是定义,没有理由,记住就行。 103 | 104 | 在数学语境中,需要注意卷积和互相关的区别。在计算机科学领域中就无需纠结了,你大可以图方便**不翻转**,平时我们说卷积时也不关心翻不翻转。 -------------------------------------------------------------------------------- /060~069/063【数据结构】哈希表.md: -------------------------------------------------------------------------------- 1 | **哈希表** (散列表, Hash Table):根据键而直接访问在内存储存位置的数据结构。 2 | 3 | 4 | 5 | # 哈希表 6 | 7 | ## 原理简述 8 | 9 | 如果我们使用朴素的方式储存数据,例如储存大量姓名,当我们要查找某个姓名时,只能够从头到尾遍历来查找,这样的查找效率非常低。但是我们可以将姓名进行分类储存,例如按姓氏分类,当查找时到对应姓氏的类别查找,这样就能大大提高查找效率。 10 | 11 | 哈希表的原理和上述例子类似,我们使用一个哈希函数(散列函数)$y=f(x)$,将建立原数据 $x$ 到类别 $y$ 的一个映射,这样便可找到数据对应的类别。上述例子的哈希函数即 $f(x)=x的姓氏$. 12 | 13 | ## 哈希函数 14 | 15 | 以下两种为**整型**数据的通用哈希方式,字符串哈希将会在单独的文章介绍:https://io.zouht.com/142.html。 16 | 17 | ### 取模法 18 | 19 | $$ 20 | f(x)=x\bmod p\ (p\in\mathbb{N^*}) 21 | $$ 22 | 23 | 我们可以非常简单地通过取模,将大范围的数据映射到 $[0,p)$ 的一个小范围内,从而实现哈希表。其中需要注意的是,$p$ 最好取为一个素数。因此若我们数据规模为 $x$ 时,$p$ 最好取为 $\geq x$ 的第一个素数。 24 | 25 | 另外 C/C++ 负数取模得非正数,需要特殊处理,即程序中哈希函数需要写为 $f(x)=(x\bmod p+p)\bmod p$ 来保证答案非负。 26 | 27 | ### 相乘法 28 | 29 | $$ 30 | f(x)=\lfloor x\cdot p\cdot c\rfloor\bmod p\ (p\in\mathbb{N^*},c\in\mathbb{R}) 31 | $$ 32 | 33 | 其中 $p$ 无特殊要求,$c$ 推荐取为黄金比例 $\varphi=\frac{1+\sqrt{5}}{2}$. 34 | 35 | ## 冲突处理 36 | 37 | 由于哈希函数将大范围数据映射到小范围内,因此冲突不可避免。如使用哈希函数 $f(x)=x\bmod 11$,$1$ 与 $12$ 均会映射到 $1$,产生冲突,我们通常使用两种冲突处理方式。 38 | 39 | ### 单独链表法 (Separate Chaining) 40 | 41 | 单独链表法即将同一位置的所有元素储存到一个链表内。用例子形象化表述: 42 | 43 | 使用哈希函数 $f(x)=x\bmod 11$,向哈希表中储存数据:$[1,2,11,56,23,12341,6547,312,27,44,654]$,那么如下表: 44 | 45 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 46 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 47 | | 11 | 1 | 2 | | 312 | 27 | | | | | 12341 | 48 | | 44 | 56 | 6547 | | | 654 | | | | | | 49 | | | 23 | | | | | | | | | | 50 | 51 | 哈希表中 $0\sim 10$ 位均为一个链表,链表储存着对应位置的所有数据。 52 | 53 | **代码** 54 | 55 | ```cpp 56 | /* 单独链表法 使用数组模拟链表实现 */ 57 | 58 | const int SIZE = 100003; 59 | // SIZE为取模的数,同时也是数组大小 60 | int hs[SIZE], val[SIZE], nxt[SIZE], idx; 61 | // hs哈希表储存链表头指针,val链表节点数据域,nxt链表节点指针域,idx链表长度 62 | // !!!hs每一字节需初始化为-1!!! 63 | 64 | inline int f(int x) // 哈希函数 65 | { 66 | return (x % SIZE + SIZE) % SIZE; 67 | } 68 | 69 | void insert(int x) // 将x插入表内 70 | { 71 | int y = f(x); 72 | // 下面的操作为链表头插法 73 | val[idx] = x; 74 | nxt[idx] = hs[y]; 75 | hs[y] = idx++; 76 | } 77 | 78 | bool query(int x) // 查询x是否在表内 79 | { 80 | int y = f(x); 81 | for (int i = hs[y]; i != -1; i = nxt[i]) // 遍历链表 82 | if (val[i] == x) 83 | return true; 84 | return false; 85 | } 86 | ``` 87 | 88 | (当需要删除操作时,通常不真正删除,而是使用一个 bool 数组指示元素的是否被删除) 89 | 90 | ### 开放寻址法 (Open Addressing) 91 | 92 | 开放寻址法当遇到冲突地址时,寻找下一个地址,直到找到可用地址储存数据。用例子形象化表述: 93 | 94 | 使用哈希函数 $f(x)=x\bmod 11$,向哈希表中储存数据:$[1,2,19,56,23]$,那么如下表: 95 | 96 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 97 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 98 | | | 1 | 2 | 56 | 23 | | | | 19 | | | 99 | 100 | 可以发现奇怪的数据为 $56$ 和 $23$,它们本应该都储存到 $1$ 号位。但 $1$ 号位已经被 $1$ 占用,出现了冲突,那么开始寻找下一位。$2$ 号位已经被 $2$ 占用,寻找下一位。$3$ 号位可用,那么 $56$ 储存到 $3$ 号位。储存 $23$ 时同理,只不过找到了 $4$ 号位才发现可用。 101 | 102 | 我们也能很明显发现问题,当哈希表接近满时,会频繁地出现冲突,甚至需要遍历大部分数组才可找到可用位,最坏情况时间复杂度会和遍历整个数组一样低。因此当使用开放寻址法时,经验规律得出数组大小最好开到数据规模的 $2\sim 3$ 倍。上面数据规模为 $5$,数组长度为 $11$,目测可以发现查询效率还挺不错。 103 | 104 | **代码** 105 | 106 | ```cpp 107 | // 开放寻址法 108 | const int SIZE = 200003, NONE = 0x3f3f3f3f; 109 | // SIZE为取模的数,同时也是数组大小,NONE为定义的代表空位的数字,需要不在哈希函数值域内 110 | int hs[SIZE]; 111 | // hs哈希表 !!!hs每一字节需初始化为0x3f!!! 112 | 113 | inline int f(int x) // 哈希函数 114 | { 115 | return (x % SIZE + SIZE) % SIZE; 116 | } 117 | 118 | int find(int x) // 若x在表内,返回x的位置;若x不在表内,返回x应当插入的位置 119 | { 120 | int y = f(x); 121 | while (hs[y] != NONE && hs[y] != x) // 当找到空位或找到了x就停下来 122 | { 123 | // 找不到就一直找下一位,找到最后一位再从第0位开始找 124 | y++; 125 | if (y == SIZE) 126 | y = 0; 127 | } 128 | return y; 129 | } 130 | 131 | void insert(int x) // 将x插入表内 132 | { 133 | int k = find(x); 134 | hs[k] = x; 135 | } 136 | 137 | bool query(int x) // 查询x是否在表内 138 | { 139 | int k = find(x); 140 | return hs[k] == x; 141 | } 142 | ``` 143 | 144 | (当需要删除操作时,通常不真正删除,而是使用一个 bool 数组指示元素的是否被删除) 145 | 146 | -------------------------------------------------------------------------------- /120~129/128【题目】Merge the squares.md: -------------------------------------------------------------------------------- 1 | **2023牛客暑期多校训练营4** 2 | 3 | H - Merge the squares! 4 | 5 | https://ac.nowcoder.com/acm/contest/57358/H 6 | 7 | 8 | 9 | ## 题意 10 | 11 | 给定 $n \times n$ 个 $1 \times 1$ 组成的正方形,每次可以合并相邻不超过 $50$ 个正方形变成一个大正方形。问如何通过合并得到一个大的 $n \times n$ 的大正方形,不限次数。 $1 \leq n \leq 10^3$ 。 12 | 13 | ## 题解 14 | 15 | 这道题是一道构造题,重点是找到一个通用的构造方法。 16 | 17 | 对于一个边长为 $c$ 的正方形,可以很简单地分成两个小正方形和两个小长方形,小正方形的边长为 $a,b$,面积关系满足: 18 | $$ 19 | c^2=(a+b)^2=a^2+b^2+2ab 20 | $$ 21 | 22 | 23 | 对于分出来的正方形,可以一直递归下去解决。先假定递归能够成功回来,那么边长为 $a,b$ 的小正方形就已经合好了,在这个区域内只占 $2$ 个合并名额。一次性最多合并的数量是 $50$ 个,那么分给旁边两个小长方形的合并名额便是每个 $24$ 个。 24 | 25 | 接下来的问题就是,如何判定能在 $24$ 个名额中合并好两个小长方形。这个可以用辗转相除法(欧几里得算法)解决: 26 | 27 | 28 | 29 | 先用宽做边长,切出几个正方形,再用剩下的长方形的宽做边长,切出几个正方形,一直这样下去直到全部切成正方形,最后把数目统计出来即可。如果切出的数目 $>24$,则说明不成立,如果切出的数目 $\leq 24$,则说明成立。 30 | 31 | 那对于一个边长为 $n$ 的大正方形,有多种切法,那该选择哪一种呢。由于欧几里得算法的时间复杂度为:$O(a+b)$,$a,b$ 为输入的两个数,那么可以选择预处理遍历所有的切割情况,记录下合法的切法。这个时间复杂度 $O(n^2)$ 可以接受。 32 | 33 | 预处理好后就可以开始正式计算了,使用递归分治的思想。对于边长为 $n$ 的正方形: 34 | 35 | - 如果 $n=1$:结束算法 36 | - 如果 $n>1$:在预处理的表中查询到正确切法 $a,b$ 37 | - 如果查到可以直接合并 38 | - 储存合并方式后直接结束 39 | - 如果查到需要切割 40 | - 储存合并方式 41 | - 递归处理左上 $a\times a$ 正方形 42 | - 递归处理右下 $b\times b$ 正方形 43 | - 递归处理左下 $b\times a$ 长方形 44 | - 递归处理右上 $a\times b$ 长方形 45 | 46 | 对于边长 $a,b$ 的长方形($a>b$): 47 | 48 | - 如果边长为 $0$:结束算法 49 | - 如果边长都不为 $0$: 50 | - 从切出的第 $1$ 个正方形遍历到第 $\lfloor a/b\rfloor$ 个正方形: 51 | - 递归处理每一个正方形 52 | - 递归处理剩下的长方形 $b,a\bmod b$ 53 | 54 | ## 代码 55 | 56 | ```cpp 57 | #include 58 | #define endl '\n' 59 | // #define int long long 60 | 61 | using namespace std; 62 | 63 | constexpr int MAXN = 1010; 64 | int split[MAXN]; 65 | vector> res; 66 | 67 | // a-lu b-rd 68 | bool check_split(int a, int b) 69 | { 70 | if (!b) 71 | return a <= 7; 72 | int ans = 0; 73 | while (b) 74 | { 75 | ans += a / b; 76 | a %= b; 77 | swap(a, b); 78 | } 79 | return ans <= 24; 80 | } 81 | 82 | void init_split(int n) 83 | { 84 | for (int i = 1; i <= n; i++) 85 | { 86 | for (int j = 0; j <= i / 2; j++) 87 | { 88 | // i - j >= j 89 | if (check_split(i - j, j)) 90 | { 91 | split[i] = j; 92 | break; 93 | } 94 | } 95 | } 96 | // for (int i = 1; i <= n; i++) 97 | // cout << split[i] << " \n"[i == n]; 98 | } 99 | 100 | void dfs(int x, int y, int a); 101 | void dfs_hori(int x, int y, int r, int c); 102 | void dfs_vert(int x, int y, int r, int c); 103 | 104 | void dfs_hori(int x, int y, int r, int c) 105 | { 106 | if (r < 1 || c < 1) 107 | return; 108 | int cnt = c / r; 109 | for (int i = 0; i < cnt; i++) 110 | dfs(x, y + i * r, r); 111 | dfs_vert(x, y + cnt * r, r, c % r); 112 | } 113 | 114 | void dfs_vert(int x, int y, int r, int c) 115 | { 116 | if (r < 1 || c < 1) 117 | return; 118 | int cnt = r / c; 119 | for (int i = 0; i < cnt; i++) 120 | dfs(x + i * c, y, c); 121 | dfs_hori(x + cnt * c, y, r % c, c); 122 | } 123 | 124 | void dfs(int x, int y, int a) 125 | { 126 | if (a == 1) 127 | return; 128 | res.push_back({x, y, a}); 129 | if (split[a] == 0) 130 | return; 131 | int lu = a - split[a], rd = split[a]; 132 | dfs(x, y, lu); 133 | dfs(x + lu, y + lu, rd); 134 | dfs_hori(x + lu, y, rd, lu); 135 | dfs_vert(x, y + lu, lu, rd); 136 | } 137 | 138 | void solve() 139 | { 140 | int n; 141 | cin >> n; 142 | init_split(n); 143 | dfs(1, 1, n); 144 | cout << res.size() << endl; 145 | for (int i = res.size() - 1; i >= 0; i--) 146 | cout << res[i][0] << ' ' << res[i][1] << ' ' << res[i][2] << endl; 147 | } 148 | 149 | signed main() 150 | { 151 | ios::sync_with_stdio(false); 152 | cin.tie(0); 153 | cout.tie(0); 154 | int t = 1; 155 | // cin >> t; 156 | while (t--) 157 | solve(); 158 | return 0; 159 | } 160 | ``` 161 | 162 | -------------------------------------------------------------------------------- /050~059/056【题目】K Derangement.md: -------------------------------------------------------------------------------- 1 | **AtCoder Regular Contest 144** 2 | 3 | C - K Derangement 4 | 5 | https://atcoder.jp/contests/arc144/tasks/arc144_c 6 | 7 | 8 | 9 | Time Limit: 2 sec / Memory Limit: 1024 MB 10 | 11 | Score : $600$ points 12 | 13 | ### Problem Statement 14 | 15 | You are given positive integers $N$ and $K$. Find the lexicographically smallest permutation $A = (A_1, A_2, \ldots, A_N)$ of the integers from $1$ through $N$ that satisfies the following condition: 16 | 17 | - $\lvert A_i - i\rvert \geq K$ for all $i$ ($1\leq i\leq N$). 18 | 19 | If there is no such permutation, print `-1`. 20 | 21 | ### Constraints 22 | 23 | - $2\leq N\leq 3\times 10^5$ 24 | - $1\leq K\leq N - 1$ 25 | 26 | ------ 27 | 28 | ### Input 29 | 30 | Input is given from Standard Input in the following format: 31 | 32 | > $N$ $K$ 33 | 34 | ### Output 35 | 36 | Print the lexicographically smallest permutation $A = (A_1, A_2, \ldots, A_N)$ of the integers from $1$ through $N$ that satisfies the condition, in the following format: 37 | 38 | > $A_1$ $A_2$ $\ldots$ $A_N$ 39 | 40 | If there is no such permutation, print `-1`. 41 | 42 | ------ 43 | 44 | ### Sample Input 1 45 | 46 | > 3 1 47 | 48 | ### Sample Output 1 49 | 50 | > 2 3 1 51 | 52 | Two permutations satisfy the condition: $(2, 3, 1)$ and $(3, 1, 2)$. For instance, the following holds for $(2, 3, 1)$: 53 | 54 | - $\lvert A_1 - 1\rvert = 1 \geq K$ 55 | - $\lvert A_2 - 2\rvert = 1 \geq K$ 56 | - $\lvert A_3 - 3\rvert = 2 \geq K$ 57 | 58 | ------ 59 | 60 | ### Sample Input 2 61 | 62 | > 8 3 63 | 64 | ### Sample Output 2 65 | 66 | > 4 5 6 7 8 1 2 3 67 | 68 | ------ 69 | 70 | ### Sample Input 3 71 | 72 | > 8 6 73 | 74 | ### Sample Output 3 75 | 76 | > -1 77 | 78 | ------ 79 | 80 | ### 解题笔记 81 | 82 | 首先需要找到存在这种排列的必要条件,即 $N\geq2K$。因为第 $K$ 位要么取 $K-K=0$,要么取 $K+K=2K$,由于 $0$ 不符合题意,则第 $K$ 位必须取 $2K$。要满足这个条件,必须有 $N\geq2K$。 83 | 84 | 接下来的重点就是找到通项公式。分为四种情况: 85 | 86 | - $N<2K$: 无解 87 | 88 | - $2K\leq N\leq 3K$: 89 | $$ 90 | A(i) = 91 | \begin{cases} 92 | i + K & (1\leq i \leq N-K) \\ 93 | i - N + K & (N-K < i\leq N) 94 | \end{cases} 95 | $$ 96 | 97 | - 98 | 99 | - $3K 126 | 127 | using namespace std; 128 | 129 | int main() 130 | { 131 | ios::sync_with_stdio(false); 132 | cin.tie(0); 133 | int N, K; 134 | cin >> N >> K; 135 | if (2 * K > N) 136 | { 137 | cout << -1 << endl; 138 | return 0; 139 | } 140 | int cnt = 0; 141 | while (N - cnt >= 4 * K) 142 | { 143 | for (int i = 1; i <= K; i++) 144 | cout << cnt + i + K << ' '; 145 | for (int i = 1; i <= K; i++) 146 | cout << cnt + i << ' '; 147 | cnt += 2 * K; 148 | } 149 | if (N - cnt <= 3 * K) 150 | { 151 | int i = 1; 152 | for (; i <= (N - cnt) - K; i++) 153 | cout << i + K + cnt << ' '; 154 | for (; i <= (N - cnt); i++) 155 | cout << i - (N - cnt) + K + cnt << ' '; 156 | } 157 | else 158 | { 159 | int i = 1; 160 | for (; i <= K; i++) 161 | cout << i + K + cnt << ' '; 162 | for (; i <= (N - cnt) - 2 * K; i++) 163 | cout << i - K + cnt << ' '; 164 | for (; i <= (N - cnt) - K; i++) 165 | cout << i + K + cnt << ' '; 166 | for (; i <= 3 * K; i++) 167 | cout << i - 2 * K + cnt << ' '; 168 | for (; i <= (N - cnt); i++) 169 | cout << i - K + cnt << ' '; 170 | } 171 | cout << endl; 172 | return 0; 173 | } 174 | ``` 175 | 176 | --------------------------------------------------------------------------------