国庆NOIP储备营笔记
Day1(基础算法)
讲师:余快
枚举法
例题1
给定一个数
朴素算法:枚举
优化枚举范围:只需枚举到
例题2
简要思路
for
循环 + check
判断。
首先枚举计算所有的数所需要的火柴数量(设立数组
再次,枚举所有在等式中可能出现的数(即
注意到火柴数最多为 check
判断一下火柴数是否符合条件即可。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int g[]={6,2,5,5,4,5,6,3,7,6};//每个数所需要的火柴的根数
int a[2223]={6};//先把 0 需要的火柴数处理好
bool check(int addend1,int addend2,int sum){
if(a[addend1]+a[addend2]+a[sum]+2+2==n)return true;//别忘了加上加号和等号的火柴数
else return false;
}
signed main(){
cin>>n;
for(int i=1;i<=2222;i++){//提前处理好每一个可能的加数
int t=i;
while(t){//求每个数所用的火柴棒
a[i]+=g[t%10];
t/=10;
}
}
int ans=0;
for(int i=0;i<=1111;i++)
for(int j=0;j<=1111;j++)//枚举两个加数
if(check(i,j,i+j))ans++;
cout<<ans;
return 0;
}
例题3
暴力枚举(
枚举区间的左端点,右端点,然后从左端点开始到右端点逐个加起来。
时间复杂度
前缀和优化的朴素算法(
先处理
枚举区间的左端点
时间复杂度
优化枚举范围(
如果我们固定右端点
于是我们从左到右枚举右端点
时间复杂度
悄悄说一下,其实这道题正解是 dp。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int f[200005];
int a[200005];
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
f[1]=a[1];
int ans=-LONG_MAX;
for(int i=1;i<=n;i++){
f[i]=a[i];
f[i]=max(f[i],a[i]+f[i-1]);
ans=max(f[i],ans);
}
cout<<ans;
return 0;
}
例题4
区间伸缩/尺取法:枚举区间左端点,不断往后选取数字直到出现所有 m 个画家为止。
左端点右移过程中,合法区间的右端点也一定在右移,所以右端点可以在上一次结果的基础上拓展,而不是重新由新的左端点开始枚举。
代码:太难了,不想写
模拟算法
模拟就是用计算机来模拟题目中要求的操作。
模拟题目通常具有码量大、操作多、思路繁复的特点。由于它码量大,经常会出现难以查错的情况,如果在考试中写错是相当浪费时间的。
例题1
P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布
直接按题意模拟即可
#include<bits/stdc++.h>
using namespace std;
int na[114514],nb[114514];
signed main(){
int x,a,b;
cin>>x>>a>>b;
int f=0,j=0,ans=0,bns=0;
for(int i=1;i<=a;i++)cin>>na[i];
for(int i=1;i<=b;i++)cin>>nb[i];
for(int i=1;i<=x;i++){
f++;j++;
if(f>a)f=1;
if(j>b)j=1;
if(na[f]==0&&nb[j]==1)bns++;
if(na[f]==0&&nb[j]==2)ans++;
if(na[f]==0&&nb[j]==3)ans++;
if(na[f]==0&&nb[j]==4)bns++;
if(na[f]==1&&nb[j]==0)ans++;
if(na[f]==1&&nb[j]==2)bns++;
if(na[f]==1&&nb[j]==3)ans++;
if(na[f]==1&&nb[j]==4)bns++;
if(na[f]==2&&nb[j]==0)bns++;
if(na[f]==2&&nb[j]==1)ans++;
if(na[f]==2&&nb[j]==3)bns++;
if(na[f]==2&&nb[j]==4)ans++;
if(na[f]==3&&nb[j]==0)bns++;
if(na[f]==3&&nb[j]==1)bns++;
if(na[f]==3&&nb[j]==2)ans++;
if(na[f]==3&&nb[j]==4)ans++;
if(na[f]==4&&nb[j]==0)ans++;
if(na[f]==4&&nb[j]==1)ans++;
if(na[f]==4&&nb[j]==2)bns++;
if(na[f]==4&&nb[j]==3)bns++;
}
cout<<ans<<' '<<bns;
return 0;
}
例题2
中模拟,样例字符串到后面原形毕露,直接不装了/cf
#include<bits/stdc++.h>
using namespace std;
int n,m;
int x,y;
struct node{
int a;
string name;
}jjj[1000005];
signed main(){
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>jjj[i].a>>jjj[i].name;
}
int jd=0;
for(int i=1;i<=m;i++){
cin>>x>>y;
if(jjj[jd].a==0&&x==0)jd=(jd+n-y)%n;
else if(jjj[jd].a==0&&x==1)jd=(jd+y)%n;
else if(jjj[jd].a==1&&x==0)jd=(jd+y)%n;
else if(jjj[jd].a==1&&x==1)jd=(jd+n-y)%n;
}
cout<<jjj[jd].name<<endl;
return 0;
}
技巧
写模拟题时,遵循以下的建议有可能会提升做题速度:
在动手写代码之前,在草纸上尽可能地写好要实现的流程。
在代码中,尽量把每个部分模块化,写成函数、结构体或类。
对于一些可能重复用到的概念,可以统一转化,方便处理。
调试时分块调试。模块化的好处就是可以方便的单独调某一部分。
写代码的时候一定要思路清晰,不要想到什么写什么,要按照落在纸上的步骤写。
实际上,上述步骤在解决其它类型的题目时也是很有帮助的。
习题
P1042 [NOIP2003 普及组] 乒乓球
P2670 [NOIP2015 普及组] 扫雷游戏
P3952 [NOIP2017 提高组] 时间复杂度
二分法
定义
二分的定义:在一个单调的有限数列上快速查找某一特定值的方法。
例题1
给你一个单调递增的数组,给你一个数x,问
可以用 STL 中的 lower_bound
和 upper_bound
轻松解决,不过在这里不讲。
事实上有不同二分的实现方法,刚刚接触二分的OIer经常在这个地方写错,进入死循环
//找第一个>q
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if (a[mid]<=q)l=mid+1;
else r=mid;
}
std::cout<<r;
//找第一个>=q
int l=1,r=n;
while(l<r){
int mid=(l+r)>>1;
if (a[mid]>=q)r=mid;
else l=mid+1;
}
std::cout<<r;
其实我们有最保守的写法
就是
而答案直接考虑
例题2
求一个正整数X开三次根后的结果,保留六位小数。
因为我们这里要二分实数,所以 while
循环的终止条件是:r-l <= eps
,其中
解法1:
直接输出 pow
来实现。
解法2
假设我们不知道 pow 函数,只知道二分和 sqrt 函数,可以朴素的二分查找。
代码:调挂了,有空再写。
分治
快速幂
CSP-S2023第一轮 著名题目:
现在用如下程序来计算x^n,其时间复杂度为( )。
Quick_power(x,k):
if k==1 return x;
else return Quick_power(x,k/2)*Quick_power(x,k/2)*(k&1)?x:1;
A.O(n)
B.O(1)
C.O(log n)
D.O(n log n)
答案选A
详细讲解见 Day6 数论一节
归并排序
基础分治题目,不多赘述。
逆序对
在算法领域中可以用归并排序实现,还可以用数据结构中的树状数组。
对拍
:loop
make
100fen!
baoli
fc baoli.out a.out
if errorlevel==1 pause
goto loop
make为造数据的可执行文件
100fen! 为正解的可执行文件
baoli 为自己代码的可执行文件
fc用来比较他们的输出文件,如果不同则停止。
贪心
例题1
我们把每个小矮人按照腿长与臂长的和来排序,然后用 dp (01背包)来选择小矮人。
边界
目标
转移方程:
Day2上午 搜索
DFS深度优先搜索
例题1
#include<bits/stdc++.h>
#define int long long
int ans = 0;
int a[105];
bool vis[105];
int n, m;
int dfs(int now) {
if(now == n + 1){
int sum = 1;
for(int i = 1; i <= n; i++)
if(!vis[i])sum *= a[i];
return sum >= m;
}
vis[now] = true;
ans += dfs(now + 1);
vis[now] = false;
ans += dfs(now + 1);
return 0;
}
signed main(){
std::cin >> n >> m;
for (int i = 1; i <= n; i++) std::cin >> a[i];
dfs(1);
std::cout << ans;
return 0;
}
非常基础的爆搜,本人因开返回值为 int
的函数但忘记返回(本机测试不出来)而爆零,警钟长鸣。
或者用一个指数级别的 for 循环来解决,借助位运算状压,即可通过。
#include <iostream>
#define int long long
int a[15];
int ans = 0;
signed main() {
int n, m;;
std::cin >> n >>m;
for (int i = 1; i <= n; i++) std::cin >> a[i];
for (int i = 0; i < (1 << n); i++) {
int mul = 1;
for (int j = 1; j <= n; j++) {
if(i & (1 << (j - 1))) mul *= a[j];
}
if (mul >= m) ans++;
}
std::cout << ans;
return 0;
}
例题2
算法1 爆搜(55分)
#include<bits/stdc++.h>
int ans = -1;
int n;
int a[1005][1005];
void dfs(int sum, int x, int y) {
if (x == n + 1) {
ans = std::max(ans, sum);
return;
}
dfs(sum + a[x][y], x + 1, y);
dfs(sum + a[x][y], x + 1, y + 1);
}
signed main(){
std::cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
std::cin >> a[i][j];
dfs(0, 1, 1); //O(2^n)
std::cout << ans;
return 0;
}
算法2 记忆化搜索
#include<bits/stdc++.h>
int n;
int a[1005][1005];
bool vis[1005][1005];
int f[1005][1005];
int dfs(int x, int y) {
//返回值表示从 x, y 走到下面能获得的最大的和
//发现对于特定的 x, y,dfs(x, y) 的结果是一定的
//但是搜索过程中我们可能多次运用 dfs(x, y)
if (vis[x][y]) return f[x][y];
vis[x][y] = true; //表示我们算过 dfs(x, y)
if (x == n + 1) {
return f[x][y] = 0;
}
f[x][y] = a[x][y] + std::max(dfs(x + 1, y), dfs(x + 1, y + 1));
return f[x][y];
//复杂度为状态的个数,就是 x, y 的个数
//状态数 -> 状态转移个数 n^2
}
signed main(){
std::cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
std::cin >> a[i][j];
//sum = a[1][1]
std::cout << dfs(1, 1); //O(n^2)
return 0;
}
例题3
例题4
爆搜即可通过,但要考虑的剪枝很多,时间复杂度很玄学。
剪枝策略:
-
相同的只搜一遍;
-
如果现在已经拼成的长度,在后面是不可能成功的。那么我们就认定这个分支是失败的。所以如果之后的分支再次遇见像这样的长度。我们就直接把他给返回就可以了。
-
如果刚开始拼接木棒的时候,第一根已经导致了拼接失败。那我们就可以判定当前的大分支是不可行的。至于为什么,是因为他在第一次已经不行。那么它在其他木棒中也是不可以的。
例题5
广告:ExOJ POJ题库
直接模拟翻转的过程,注意翻转时的边界条件。搜索时搜两次,同样判断边界,分别为操作以及不操作的数量,而且要注意位置的更新。当棋盘中所有位置的颜色都一样(用一个函数判断)时,维护最小的答案。如果全部搜完了答案还是原来的数值,输出 Impossible
。
#include <iostream>
char ch[5][5];
int ans = 0x7fffffff;
bool check() { //判断所有格子颜色是否都相同
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if(ch[i][j] != ch[0][0])
return false;
}
}
return true;
}
bool pdin(int x, int y) { //判断是否在棋盘内(没有越界)
return x < 4 && y < 4 && x >= 0 && y >= 0;
}
void change(int x, int y) { //单个位置修改
ch[x][y] = (ch[x][y] == 'w' ? 'b' : 'w');
}
void update(int x, int y) { //四周都要修改
change(x, y);
//修改时要判断边界
if (pdin(x + 1, y)) change(x + 1, y);
if (pdin(x, y + 1)) change(x, y + 1);
if (pdin(x - 1, y)) change(x - 1, y);
if (pdin(x, y - 1)) change(x, y - 1);
}
void dfs(int x, int y, int cnt){
if(check()) { //满足条件
ans = std::min(cnt, ans);
return;
}
if(!pdin(x, y)) return; //如果不在棋盘内,返回
int yy = (y + 1) % 4; //得到下一个位置的坐标(y为0 1 2 3)
int xx = x + (y + 1) / 4; //x 的值,取决于y + 1是否等于4,等于则x + 1
dfs(xx, yy, cnt);
update(x, y);
dfs(xx, yy, cnt + 1);
update(x, y);
//搜两种情况。注意最后要还原
}
signed main() {
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
std::cin >> ch[i][j];
dfs(0, 0, 0);
if (ans == 0x7fffffff) std::cout << "Impossible"; //没有情况
else std::cout << ans;
return 0;
}
例题6
搜索 + 大模拟,太麻烦不说了。
例题7
dp或搜索,前置知识为二次函数和二元一次方程组。
折半搜索法
如果问题能分成两个相对独立的部分,且两部分搜索结果能很方便合并,那么就可以利用折半搜索来大大提高搜索的效率。
我们先搜前半部分,然后将前半部分的搜索结果存入 hash 表,再去搜后半部分,再把两次搜索结果合起来。
BFS宽度优先搜索
BFS可以直接用来求每条边边权为
用来求每条边边权为
注意要用双端队列 deque
来存,在队首插入
例题1
思路:宽度优先搜索
作为BFS例题,有必要贴一下代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+10;
vector<int>z[MAXN];
int dep[MAXN],cnt[MAXN];
bool vis[MAXN];
void add_edge(int s,int e){
z[s].push_back(e);
}
signed main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
add_edge(u,v);
add_edge(v,u);
}
queue<int>q;
q.push(1);
dep[1]=0;
vis[1]=true;
cnt[1]=1;
while(q.size()){
int p=q.front();
q.pop();
for(auto v:z[p]){
if(!vis[v]){
vis[v]=true;
q.push(v);
dep[v]=dep[p]+1;
}
if(dep[v]==dep[p]+1)
cnt[v]=(cnt[v]+cnt[p])%100003;
}
}
for(int i=1;i<=n;i++)
cout<<cnt[i]<<'\n';
return 0;
}
Day2下午 数据结构
二叉搜索树
二叉搜索树的性质
二叉搜索树具有以下性质:
- 这是一颗二叉树
- 每个节点的左儿子比自己小
- 每个节点的右儿子比自己大
如图,这两个都是二叉搜索树。可能会是一条链,所以容易被卡掉(极端情况下可能和用 for 循环没啥区别)。
节点建立
const int MAXN = 100005;
const int L = 0, R = 1; //定义常量,方便代码编写
struct node{ //一个树上节点
int son[2]; //记录左右儿子的下标,分别对应 son[L], son[R]
int val; //这个节点储存的值
}a[MAXN];
int cnt; //决定分配节点编号
二叉搜索树该如何建立呢?
- 将节点从根节点开始插入
- 与当前节点比较大小
- 如果比当前节点储存的值大,向右递归
- 如果比当前节点储存的值小,向左递归
关于二叉搜索树
二叉搜索树有什么用呢?
没用
那为什么要学习二叉搜索树呢?
非常简单的二叉树结构,方便大家熟悉二叉树如何编写,以及二叉树的构成。
——吴凯路
模板
二叉堆
二叉堆的定义
-
二叉堆是一种特殊的二叉树
-
满足根节点比任意节点 大 / 小 (决定了是大根堆还是小根堆)
二叉堆的作用
一般二叉堆能解决什么问题呢?
在
在
在
节点建立
需要储存左儿子,右儿子,自己的权值,子树大小
const int MAXN = 100005;
const int L = 0, R = 1; //定义常量,方便代码编写
struct node{ //一个树上节点
int son[2]; //记录左右儿子的下标,分别对应 son[L], son[R]
int val; //这个节点储存的值
int size; //这个节点对应的子树大小
}a[MAXN];
int cnt; //决定分配节点编号
插入操作
以大根堆为例
- 从根节点开始,插入一个值,如果当前值比根节点大,与根节点交换存储的值
- 然后往子树大小更小的儿子递归
- 直到某个儿子大小为0(即没有某一边儿子),新建一个节点来储存这个值
- 这样我们可以保证插入操作一定不超过log n次递归。
删除操作
以大根堆为例
- 将当前节点权值视为0,与最大的儿子交换权值并递归
- 直到节点是一个叶子(无左右儿子),然后删除该叶子。
- 这样我们可以保证删除操作一定不超过log n次递归。
- 删除根节点即可弹出最大值。
查询最大值操作
以大根堆为例
取根节点的权值即可。
STL中的堆——priority_queue
支持一切常用操作。
薄纱手写堆了属于是
模板
习题
灵活运用:
洛谷 P1090
洛谷 P1631
洛谷 P2085
线段树
信息学竞赛中最最最重要的数据结构,最最最常见的数据结构 ——吴凯路
线段树的作用
线段树解决什么问题呢?
各种各样的序列操作问题,例如最常见的问题:
有一个长度为
然后有
修改一个位置的值
查询一个区间的权值和
线段树的定义
线段树是一种二叉树结构
线段树上每个节点对应一个区间
节点的左儿子对应
运用
如何构建线段树?
首先令根节点的范围为
一共需要
不过有特殊写法,动态开点只需要开二倍空间即可。
接下来写的是吴凯路老师讲的线段树,个人感觉很抽象()
单点修改
从根开始向下递归,用二分的思路找这个点,然后找到包含这个点的所有区间,修改对应区间的统计值。
区间查询
- 查询的时候将区间作为参数传入
- 如果查询区间是当前区间的,那么返回当前节点的统计值
- 如果查询区间是当前区间的一个子区间,根据将查询区间按情况递归进入左儿子,或者右儿子或者两边。
- 这样可以把一个查询区间拆成已有的不超过
个区间。
大家一定这么感觉:太简单了,只要学过分治不就能写出来吗?
难度提升警告
有一个长度为
然后有
给某个区间的所有位置加上一个权值
查询某个区间的权值和
区间修改 & lazy标记
有这样一个问题,有一个长度为10的区间,和为205,现在对于区间中每一个位置增加了7,那么和为多少?
可以计算和为
我们知道每个位置的具体值吗? 并不知道
与每个位置的具体值有关吗? 无关
所以我们使用类似于区间查询的方法,将每个区间修改操作,拆成
然后单个节点可以自己更新自己的统计值(比如区间和),不需要下发这个操作到所有子节点。
但是我们需要记录当前有多少尚未下发的区间加操作。(即所有未下发的区间加操作的和)因为如果查询的不是整个区间,而是某个更小的子区间,那么会对答案造成影响。
所以我们有了下传操作(pushdown)。
下传标记即为将未下发的操作合并后变成一个操作一起下发给左右儿子。(注意只下发给左右子儿子就够了)
然后清空当前节点暂存的操作。(因为已经下传走了)
例题1
#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define LL long long
const int L=0,R=1;
LL v[N];
struct xds{
int son[2];
LL sum;//区间和, 随时随地的区间总和,已考虑了addtag
LL addtag;//区间加标记 现在每个人加了addtag分
}a[N*2];int cnt;
void build(int &k,int l,int r){//建立线段树
k=++cnt;
if(l==r){
a[k].sum=v[l];//初始化信息
}else{
int mid=(l+r)>>1;
build(a[k].son[L],l,mid);//递归建立左儿子
build(a[k].son[R],mid+1,r);//递归建立右儿子
a[k].sum=a[a[k].son[L]].sum+a[a[k].son[R]].sum;//合并左右儿子信息
}
}
// 给节点k,增加一个标记(告诉他所有人加分)
// 给节点k说,他管理的范围是[l,r],然后所有人增加 val 分
void addtag(int k,int l,int r,LL val){//节点k上做区间加
a[k].sum+=(r-l+1)*val; // 显然管理区间的总分增加了 人数 * val
a[k].addtag+=val; // 记录一下当前的tag
}
void pushdown(int k, int l, int r){
int mid = (l+r) >> 1;
addtag(a[k].son[L],l,mid,a[k].addtag);
addtag(a[k].son[R],mid+1,r,a[k].addtag);
a[k].addtag=0;
}
// 节点k,管理[l,r],对于区间[ql, qr] 集体增加 val
void modify(int k,int l,int r,int ql,int qr,LL val){//区间修改操作,比如区间加
if(ql==l&&r==qr){//全区间操作
addtag(k,l,r,val);
}else{
int mid=(l+r)>>1;
//下传并清空节点k的所有标记
pushdown(k, l, r);
if(qr<=mid) modify(a[k].son[L],l,mid,ql,qr,val);
else if(ql>mid) modify(a[k].son[R],mid+1,r,ql,qr,val);
else modify(a[k].son[L],l,mid,ql,mid,val),modify(a[k].son[R],mid+1,r,mid+1,qr,val);
a[k].sum=a[a[k].son[L]].sum+a[a[k].son[R]].sum;//下传后更新
}
}
LL query(int k,int l,int r,int ql,int qr){//区间查询操作,比如问区间和
if(ql==l&&r==qr){//全区间查询
return a[k].sum;
}else{
int mid=(l+r)>>1;
//下传并清空节点k的所有标记
pushdown(k, l, r);
LL ret = 0;
if(qr<=mid) ret=query(a[k].son[L],l,mid,ql,qr);
else if(ql>mid) ret=query(a[k].son[R],mid+1,r,ql,qr);
else ret=query(a[k].son[L],l,mid,ql,mid)+query(a[k].son[R],mid+1,r,mid+1,qr);
return ret;
}
}
int n, m, root;
int main(){
scanf("%d%d", &n, &m);
for(int i=1;i<=n;i++)
scanf("%lld", &v[i]);
build(root, 1, n);
for(int i=1;i<=m;i++){
int op, x, y;
LL k;
scanf("%d%d%d", &op, &x, &y);
if(op == 1){
scanf("%lld", &k);
modify(root, 1, n, x, y, k);
}else{
printf("%lld\n", query(root, 1, n, x, y));
}
}
return 0;
}
吴凯路老师的存树方式是动态开点,并且没有存
线段树1——Lht(Pascal and C++)
例题2
在结构体中加一个 mul,即为乘法的lazy标记,和加法的lazy标记一样,用来更新区间答案以及打上/下传标记。
注意,在进行下传操作时,要先下传乘法的标记,再下传加法的标记,因为这样可以更方便地更新/获取答案
树状数组
树状数组与线段树
线段树是一种解决区间修改与区间查询的数据结构
树状数组是一种简单高效的解决单点修改与前缀查询的数据结构
也就是说,树状数组能实现的功能,线段树都可以
但是线段树通常比树状数组慢一倍时间,代码长三倍。
其实,树状数组可以看成线段树的 “青春版”。
树状数组的优劣势
-
优势:代码量短,速度较快。
-
劣势:树状数组只能支持单点修改和前缀查询,并不能支持区间修改和区间查询
树状数组的实现
例如
树状数组的节点i,管理的区间是
从二进制角度考虑:
树状数组可以把任意前缀区间拆分成log n个已有区间。
所以树状数组可以支持一些单点修改,前缀和查询。
例如修改单点位置的值,求某一个前缀和。
代码:
const int N = 1000006;
#define low(x) ((x)&(-(x)))
int bits[N]; // 装着每个树状数组节点管理区间的区间和
void modify(int x,int val){//a[x]+=val
for(;x<N;x+=low(x)) bits[x]+=val; // 区间和增加 val
}
int query(int x){// 查询 [1,x] 的和
int ret=0;
for(;x!=0;x-=low(x)) ret+=bits[x];
return ret;
}
// [L, R] ?
// [1, R] - [1, L-1]
query(R) - query(L-1)
for(int i=1;i<=n;i++) modify(i, a[i]);// 把初始化变成n次单点修改
例题1
树状数组模板题。
简洁的代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1000006;
#define low(x) ((x)&(-(x)))
int bits[N]; // 装着每个树状数组节点管理区间的区间和
void modify(int x,int val){//a[x]+=val
for(;x<N;x+=low(x)) bits[x]+=val; // 区间和增加 val
}
int query(int x){// 查询 [1,x] 的和
int ret=0;
for(;x!=0;x-=low(x)) ret+=bits[x];
return ret;
}
int n, m;
int a[N];
signed main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++){
scanf("%d", &a[i]);
modify(i, a[i]); // 逐个加入ai完成树状数组初始化
}
for(int i=1;i<=m;i++){
int op,x,y;
scanf("%d%d%d", &op, &x, &y);
if(op==1) modify(x, y);
else{
int ans=query(y)-query(x-1); // [x,y] = [1,y] - [1,x]
printf("%d\n", ans);
}
}
return 0;
}
Day3上午 数据结构
RMQ问题
给出一个长度为
线段树
时间复杂度
#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define LL long long
const int L=0,R=1;
LL v[N]; // RMQ 问题还是会给出每个人的初始分数
struct xds{
int son[2];
LL maxv, minv; // 变成维护区间最大值最小值
// 没有修改,那么没有任何tag
}a[N*2];int cnt;
void update(int k){ // 更新一个点的信息
a[k].maxv = max(a[a[k].son[L]].maxv, a[a[k].son[R]].maxv);
a[k].minv = min(a[a[k].son[L]].minv, a[a[k].son[R]].minv);
}
void build(int &k,int l,int r){//建立线段树
k=++cnt;
if(l==r){
a[k].maxv = v[l];
a[k].minv = v[l];
}else{
int mid=(l+r)>>1;
build(a[k].son[L],l,mid);//递归建立左儿子
build(a[k].son[R],mid+1,r);//递归建立右儿子
update(k);//合并左右儿子信息
}
}
LL query_min(int k,int l,int r,int ql,int qr){//区间查询操作,比如问区间和
if(ql==l&&r==qr){//全区间查询
return a[k].minv;
}else{
int mid=(l+r)>>1;
//下传并清空节点k的所有标记
LL ret = 0;
if(qr<=mid) ret=query_min(a[k].son[L],l,mid,ql,qr);
else if(ql>mid) ret=query_min(a[k].son[R],mid+1,r,ql,qr);
else ret=min(query_min(a[k].son[L],l,mid,ql,mid), query_min(a[k].son[R],mid+1,r,mid+1,qr));
return ret;
}
}
LL query_max(int k,int l,int r,int ql,int qr){//区间查询操作,比如问区间和
if(ql==l&&r==qr){//全区间查询
return a[k].maxv;
}else{
int mid=(l+r)>>1;
//下传并清空节点k的所有标记
LL ret = 0;
if(qr<=mid) ret=query_max(a[k].son[L],l,mid,ql,qr);
else if(ql>mid) ret=query_max(a[k].son[R],mid+1,r,ql,qr);
else ret=max(query_max(a[k].son[L],l,mid,ql,mid), query_max(a[k].son[R],mid+1,r,mid+1,qr));
return ret;
}
}
int n, m, root;
int main(){
scanf("%d%d", &n, &m);
for(int i=1;i<=n;i++)
scanf("%lld", &v[i]);
build(root, 1, n);
for(int i=1;i<=m;i++){
int op, x, y;
LL k;
scanf("%d%d%d", &op, &x, &y);
if(op == 1){ //op=1 表示查询区间 [x,y] 最小值
printf("%lld\n", query_min(root, 1, n, x, y));
}else{ //op=2 表示查询区间 [x,y] 最大值
printf("%lld\n", query_max(root, 1, n, x, y));
}
}
return 0;
}
ST表
优势:在
前提算法——倍增法。
实现:
-
定义
表示以 为开头,长度为 的区间的最大(小)值,即对应于 。 -
首先我们思考递推边界,显然是对于所有的
都赋值为 。为什么?根据 数组的定义, 所对应的区间是 。诶,这不就一个数吗?那最大(小)值肯定是他自己( )了。 -
在递推时,我们把子区间的长度倍增,不难发现通过
可以推出 ,有公式 ,即长度为 的子区间的最大值是左右两半长度为 的子区间的最大值中较大的一个。下图为ST表:
查询时,我们要处理一个
为了方便查找,我们要预处理
其实还有一种方法,就是查的时候直接利用 cmath 库中的 log
函数(自然对数,即以常数 log2
函数(以 log
和 log2
的常数可能较大,不能保证严格
代码(wkl):
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
inline int read(){
int x=0,f=1; // x 表示我们读到值, f表示数的符号
char ch=getchar(); // getchar 是获得输入内容的下一个字符
while (!isdigit(ch)){
if (ch=='-') f=-1; // 说明输入一个负数
ch=getchar(); // 不然的话下一个字符
}
while (isdigit(ch)){
x=x*10+(ch-'0'); // x=123, x=1234 = 123 * 10 + 4
ch=getchar(); // 读下一个字符
}
return x*f; // 数字乘上符号
}
const int INF = 0x3f3f3f3f; // 大概是 10的9次方多一些 INF+INF < 2^31
int n,m; // 假设我们序列长度为 n,一共有m个询问。
int a[N]; // a是初始值序列
//ST表
int st[20][N]; // st[i][L] 表示从 L 开始长度为 2^i 的区间的最大值
int logn[N]; // logn[i] 表示 2^logn[i] 次方不超过 i
// logn[1] = 0, logn[2] = 1, logn[3] = 1, logn[4] = 2, logn[5] = 2, logn[6]=2, logn[7]=2, logn[8]=3
// logn[i] = logn[i/2] + 1
void pre(){
for(int i=2;i<=n+2;i++) logn[i]=logn[i>>1]+1; // 预处理一下logn数组
for(int i=1;i<=n;i++) // 初始化st 表第一行 st[0], 即长度为1的序列
st[0][i]=a[i];
for(int i=1;(1<<i)<=n;i++) // (1<<i) = 2的i次方,
for(int j=1;j+(1<<i)-1<=n;j++){ // j 是区间起始位置, j+(1<<i)-1 是区间结尾
st[i][j]=max(st[i-1][j],st[i-1][j+(1<<(i-1))]);
// 例如 i=2, j=3, st[2][3] 表示区间 [3,6]
// 那么就是 st[2][3] = max(st[1][3], st[1][5]);
// 其中st[1][3] 表示区间 [3, 4], st[1][5] 表示区间 [5, 6]
}
}
int getmax(int l,int r){ // 查询[l, r]
if(l>r) return -INF; //
int d=logn[r-l+1]; // 不超过区间长度的最大的2的整数次幂
return max(st[d][l],st[d][r-(1<<d)+1]); // l开始长度为2的d次方的,和结束于r的长度为2的d次方的
}
int main(){
n = read(), m = read();
for(int i=1;i<=n;i++) a[i]=read();
pre();
for(int i=1;i<=m;i++){
int l=read(), r=read();
printf("%d\n", getmax(l,r));
}
return 0;
}
LCA
树是什么?
-
个点, 条边的连通图。或者说,没有环的连通图。 -
有根树,无根树。
-
树根,树上节点的父亲、儿子、祖先、后代、兄弟
-
一个节点的深度定义为到根节点的距离
如何存储一棵树?
使用 vector[i]
存储与节点
如何遍历一棵树?
从树根开始,DFS 递归遍历,每次遍历记录父亲是谁,避免死循环。
LCA(最近公共祖先)问题:
有一颗N个节点的树,有M次询问,每次询问给出两个点
节点
反之,如果
解法1:
转换为 RMQ 问题。
先遍历一遍树,生成 欧拉遍历序。
我们记录给每个点它在欧拉遍历序中的任意一个出现位置
那么查询
例子:
欧拉遍历序:
对应点深度:
数组下标:
比如查询
区间中深度最小值为
解法2:
树上倍增法。
预处理:
由于向上走
即因为
所以
预处理时同时记录每个节点的深度
查询时先通过倍增法使得深度更深的点,向上走到与另一个点相同的深度,然后再次运用倍增法走到两个点深度最浅的不同的祖先。
比如在下图中查询
首先从
然后
于是
代码(wkl):
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=110; // 最多的点数
const int maxm=210; // 最多的边数
int n;
vector<int> g[maxn]; // g[i] 存着与i相连的点
void AddEdge(int u,int v) { // 添加一个(u,v)双向边
g[u].push_back(v);
g[v].push_back(u);
}
int fa[20][maxn],dep[maxn]; // fa[i][k] 表示k节点往上走 2的i次方步到哪个祖先
// dep 存的是节点深度
void dfs(int x, int father){ // 树上遍历建立倍增表
fa[0][x] = father; // 记录一下
dep[x] = dep[father]+1; // 根节点深度为 1
for(int i=1;(1<<i)<=dep[x];i++)
fa[i][x]=fa[i-1][fa[i-1][x]];
for(auto to: g[x]) //遍历自己的儿子
if(to != father) // 别跑回父亲去了
dfs(to, x);
}
int lca(int x,int y) {
if(dep[x]<dep[y]) swap(x,y); // 先保证 x的深度比y的深度深
// 假设 dep[x] - dep[y] = 6 = 4 + 2
// 6 的二进制是 0110,对应了 4 (0100) 和 2(0010)
for(int i=19;i>=0;i--)
if( (dep[x]-dep[y]) & (1<<i)) // 拿2的整数次幂凑出我们要走的距离
// if( (dep[x]-dep[y]) >= (1<<i)) // 等效写法,能走就走
x=fa[i][x];
if(x == y) return x; // 如果某一个人是另一个人的直接祖先,返回
for(int i=19;i>=0;i--) // 倍增找深度最浅不相同的祖先
if(fa[i][x] != fa[i][y])
x=fa[i][x], y=fa[i][y];
return fa[0][x]; // 返回某个的直接父亲就是最近公共祖先
}
int main() {
n=5;
AddEdge(1,2);
AddEdge(2,3);
AddEdge(2,5);
AddEdge(5,6);
AddEdge(1,4);
AddEdge(4,7);
AddEdge(7,8);
AddEdge(7,9);
dfs(1,0);
printf("%d\n",lca(3,9));
return 0;
}
LCA——Lht
Day3下午 动态规划
dp入门
动态规划是什么
- 解决一类问题的方法
- 由三部分组成:有限状态 、状态值函数、状态转移方程
状态
-
状态就是能够恰好表达当前需要的信息的一组数据
-
我们只记录关键的变化量
-
不记录对答案没有影响的状态和绝对不变的数据。
-
通过已知信息和状态里可以推导出的数据不用记录。
举例:
扫雷大家一定都玩过,我们以此游戏为例来介绍 dp 中的状态。
注意,我们并没有记录用户扫雷点方块的顺序,因为这不重要,不影响游戏的继续和已有的得分。
以及一些绝对不变的数据,比如这个棋盘的尺寸和雷的位置,也不用记录,这些开始就是已知信息。
还有扫雷时每个位置的数字(比如
而围棋,总共有
会少一些,因为存在一个位置不能下或棋子没有气的情况。
状态转移方程
既然状态都被我们列举出来了
我们可以通过另一些状态计算某个状态的对应值
例如对于一个
设
初始状态是所有位置都没被翻开的局面。
例题1
山用一个三角形表示,从山顶依次向下有
输出: 一个数,即小猪到达山顶所需要的最短时间。
状态
状态转移方程:
其中
那山脚怎么处理?
根据题意,直接初始化山脚
因为我们输入的三角形是对齐的,所以原来的右上位置就相当于对齐后的正上,即
注意,当
以及
#include <bits/stdc++.h>
int n,T[1005][1005];
int f[1005][1005];
signed main() {
std::cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
std::cin >> T[i][j];
for (int i = 1; i <= n; i++) f[n][i] = T[n][i];
for (int i = 1; i <= n; i++) //分类讨论
for (int j = 1; j <= i; j++) {
if (j == 1)
f[i][j] = f[i - 1][j] + T[i][j];
else if (i == j)
f[i][j] = f[i - 1][j - 1] + T[i][j];
else
f[i][j] = std::min(f[i - 1][j - 1], f[i - 1][j]) + T[i][j];
}
int minn = 1e18;
for (int i = 1; i <= n; i++)
minn = std::min(minn, f[n][i]);
std::cout << minn;
return 0;
}
例题2
有一个
设计状态:
初始值
#include <bits/stdc++.h>
#define int long long
const int MAXN = 1e3 + 5;
int n, m;
int val[MAXN][MAXN];
int f[MAXN][MAXN];
signed main() {
std::cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
std::cin >> val[i][j];
memset(f, 0, sizeof(f));
for(int i = 1;i <= n; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = max(dp[i - 1][j], dp[i][j -1]) + val[i][j];
std::cout << f[n][m];
return 0;
}
dp与搜索
dp与搜索的关系
-
动规和搜索都是求解状态值函数的方法
-
一个递推、一个递归
-
一个重复值只算一次,一个重复值可能算多次
-
只要能搜就能动规,只要能动规也能搜索
//例题2的搜索版本
int dfs(int x,int y){ //这就是需要的状态表示,返回状态值
a1=dfs(x-1,y); //这个是状态转移
a2=dfs(x,y-1);
return max(a1,a2)+a[x][y];
}
上图这是以上代码的搜索树,肉眼可见,这个搜索树会指数级增大,而且有大量的重复。
我们可以记录下每个状态得到的答案,这样第二次搜索到时,直接返回答案,
这就是记忆化搜索,本题搜索复杂度从
// dp版本
for(int i = 1;i <= n; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = max(dp[i - 1][j], dp[i][j -1]) + val[i][j];
// 两个循环完美满足所有要求。(其实大多数都是可以几个循环解决)
从前往后的递推,每个状态只算一次。
状态转移方程的补充说明:
状态转移方程需要满足如下条件:
-
除了初始状态以外,每个状态都可以通过其他状态计算得出
-
依赖关系不能成环(成环了,该按照什么顺序算???)
我们还忘了一个dp的关键要素——状态值函数
通常,我们通过拓扑序遍历所有状态来逐个计算状态值函数。
Day5 图论
图的概念
有点有边的叫做图。
有向图:边有方向
无向图:边没有方向
度:一个顶点的度是指于该顶点相关联的边的条数(定义在无向图中)
对于有向图来说,一个顶点的度可细分为入度和出度
入度:与其关联的各边中,以其为终点的边数
出度:以改顶点为起点的边数。
自环:若一条边的两个顶点为同一顶点,则此边为自环。
路径:从一个顶点到另一顶点的可能经过同一个点和同一条边的路线
简单路径:不能走重复点和重复边的路径。
环:起点 = 终点的路径。
简单环:简单路径和环的集合体,保证起点 = 终点且除起点(终点)外不能经过重复的点和边的路径。
树相关
树:无向、无环、联通(任意两点间都可以互相到达)。
如果我们去除一些性质会变成什么样?
去掉联通:
森林:无向、无环、但不连通(有很多树组成)。
去掉无向:
有向树:有向、无环、联通。
外向树:所有的边都向外处指的有向树。
内向树:所有的边都通向一个点的有向树。
去掉无环:
章鱼图 / 基环树:无向、联通且只有一个环。
仙人掌:无向、联通且有多个环。
其他图
DAG:有向无环图(在树形dp中常用)
二分图:把无向图的所有的点分为两个部分,第一部分的点连接的一定是第二部分的点,第二部分的点连接的一定是第一部分的点。也就是说一条边一定是连接第一部分和第二部分的点。不要求联通。树和森林就是二分图。在树中,深度为奇数的为第一部分,深度为偶数的为第二部分。
奇环:长度为奇数的环。
存在奇环的不是二分图,所有的二分图也不存在奇环。
存图
邻接矩阵
const int MAXN=1010;
//邻接矩阵
//缺点:需要n^2的内存
//优点:取i->j这条边的信息只需要O(1)
int bian[MAXN][MAXN];
//bian[i][j]表示从i点到j点这条边的信息
void add_edge(int s,int e,int d){
bian[s][e]=d;
}
边表
const int MAXN=1010;
//vector存图:边表
//缺点:取i->j这条边的信息只需要O(n)
//优点:这需要O(n+m)的内存
vector<pair<int,int> >g[MAXN];//g[i]用来储存所有从i出发的边
void add_edge(int s,int e,int d){//添加一条从s->e长度为d的边
g[s].push_back(make_pair(e,d));
}
二分图的判定——染色法
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1005;
int n,m;
vector<int>z[MAXN];
int col[MAXN];
//col[i]==0 i点还没决定放哪边
//col[i]==1 i点放左边
//col[i]==2 i点放右边
void add_edge(int s,int e){
z[s].push_back(e);
z[e].push_back(s);
}
signed main(){
cin>>n>>m;//n个点m条边
for(int i=1;i<=m;i++){
int s,e;
cin>>s>>e;
add_edge(s,e);
}
for(int i=1;i<=n;i++){//for循环不断寻找
if(col[i]==0){//还没被染过色
col[i]=1;//放左边
queue<int>q;//储存可以改变周围点颜色的点
q.push(i);
while(q.size()){
int p=q.front();
q.pop();
for(int i=0;i<z[p].size();i++){//可以与27行重复,因为定义域不同
int j=g[p][i];//这是一条从p到j的边
if(col[j]==0){//没有过染色
col[j]=3-col[p];
q.push(j);
}
else if(col[p]==col[j]){//相邻点的颜色相同,说明不是二分图
cout<<"No";
return 0;
}
}
}
}
}
cout<<"Yes";
return 0;
}
拓扑排序
我们要记录每个点的入度。首先把入度为
整体思路就是如果图中有入度为
#include<bits/stdc++.h>
#define gt getchar
#define pt putchar
#define ll long long
#define ull unsigned long long
using namespace std;
const int MAXN=105;
vector<int>z[MAXN];//边表
void add_edge(int s,int e){
z[s].push_back(e);
}
int ind[MAXN];///ind[i]代表i点的入度
int result[MAXN];//result数组储存最后的排序结果
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
int s,e;
cin>>s>>e;
add_edge(s,e);
ind[e]++;//入度增加
}
int cnt=0;
for(int i=1;i<=n;i++)
if(!ind[i])result[++cnt]=i;//把入度为0的点先算入结果
for(int i=1;i<=n;i++){
int p=result[i];//取一个已被算入结果(入度为0)的点
for(int j=0;j<z[p].size();j++){//枚举入度为0的点所连接的点
int q=z[p][j];
ind[q]--;//把这个点的入度减去1
if(!ind[q])result[++cnt]=q;//如果这个点的入度被减到0了,那么算入结果
}
}
for(int i=1;i<=n;i++)cout<<result[i]<<' ';
return 0;
}
最短路问题
形式化的,最短路的定义就是从一个点到另一个点的所有路径中所经过的边边权之和最小的路径。
最短路问题一般分为单源最短路问题和多源最短路问题。
单源最短路问题:一个起点到其他点的的最短路,即起点是固定的。
多源最短路问题:多个起点到其他点的的最短路,起点不固定。
假如我们用
这就是整个最短路算法中最重要的式子,显然有
我们把这个式子叫作最短路的三角不等式。之所以叫这个名字,是因为连接
到此为止,整个最短路的理论部分就讲完了。
Floyd
本质上是一个动态规划。
状态:
目标值:最后求出
初始化:
-
如果
到 有边, 。 -
如果
到 无边, 。
那剩下
,表示一条从 到 经过点的编号小于 的最短路,如果不存在则为 。 ,先从 到 再从 到 ,把两个最短路相加(这两条路所经过点的编号必然小于 ),如果其一不存在则为 。
最后 的最短路就是两者取最小值,这两种情况其实也就是不经过 和经过 的情况。
容易写出这个 dp 的状态转移方程是 。
Floyd算法的时间复杂度为
Dijkstra算法
如果想要使用Dijkstra算法,就必须保证所有边的边权都为正数。
确定出发节点
我们首先把
一个点的松弛操作就是用自己已经求出的最短路去更新其他点的最短路,进行完松弛操作后我们要用一个
当
我们用
我们可以用一个 pair 类型的堆来维护目前
这样优化过后,时间复杂度为
Bellman-Ford算法
首先我们需要引入一个小结论:——个点到另一个点的最短路最多经过
我们的Bellman-Ford算法算法就是基于这个结论的,我们首先用三个数组读入这张图,把
每次操作用一条边起点的最短路去更新这条边终点的最短路,即
SPFA算法
SPFA算法的正式名称叫做队列优化的Bellman-Ford算法,顾名思义它的核心是一个队列,用来储存\textbf{可能}改变其他点最短路的点,还是用
操作结束的终止条件是队列为空,因为如果没有点能再进行松弛操作就说明所有点的最短路已经求出来了。
SPFA算法的最坏时间复杂度和Bellman-Ford算法一样是
如果出题人就是想卡你怎么办?我们可以不按照题目要求,把读进来的所有边 random_shuffle
一下,玄学改变搜索顺序可以使速度更快。
切忌在边权都为正的时候使用SPFA算法,但如果边权有为负数的,SPFA就是最好的选择。
生成树
定义
从一张
Kruskal算法
求最小生成树把所有边记录下来后按边权排序,然后把边依次取出加入到树中,用并查集判环,到最后取不到
时间复杂度
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+10;
int to[MAXN];//to[i] i点在并查集里面的箭头指向谁
int go(int p){//看一下点p沿着并查集最后会走到哪里
if(to[p]==p)return p;
else return to[p]=go(to[p]);
}
struct edge{
int s;
int e;
int d;
}g[MAXN];
bool cmp(edge a,edge b){
return a.d<b.d;
}
int ans=0;
signed main(){//O(mlogm)
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++)
cin>>g[i].s>>g[i].e>>g[i].d;
sort(g+1,g+m+1,cmp);//所有边 边权从小到大排序
for(int i=1;i<=n;i++)to[i]=i;
long long ans=0;//生成树大小
int cnt=0;//记录边的数量
for(int i=1;i<=m;i++){
if(cnt==n-1)break;
if(go(g[i].s)!=go(g[i].e)){
ans+=g[i].d;
to[go(g[i].s)]=go(g[i].e);
cnt++;
}
}
if(cnt!=n-1)cout<<"orz";
else cout<<ans;
return 0;
}
强连通分量
定义
在有向图中,找到若干个点和若干条边,使得每个点都可以互相走到,构成的图就叫做强连通分量。独立的点也可以作为一个强连通分量。每个点都一定要在一个强连通分量里。
问题:给你一张有向图,找到所有的强连通分量。
tarjan算法
树边:遍历过程中正常经过的边。
找能组成环的非树边。
什么样的非树边能产生环?
回边:一条非树边连向自己的祖先,能形成环。
横叉边:不是回边的边(没连向自己的祖先,不能组成环)。
如果两条横叉边能组成环,那是因为 DFS 改变了树的形态,所以那不是横叉边,而是一条回边。
横叉边虽然不能组成环,但是可以扩大环的范围,但是要求连过去的边往上走的最上面的那个点必须是横叉边开始的点的
思路:我们用
int num;//已经dfs了nun个点
int dfn[MAXN];//dfn[i]第i个点是第几个被dfs到的
int low[MAXN];
/*low[i]代表从i点开始,沿着回边 树边 或者能扩大环的横叉边走
能走到的点中dfn最小的是多少
*/
vector<int>z[MAXN];
int belong[MAXN];//belong[i] 表示 i 属于哪一个强连通分量
stack<int>s;//储存被dfs过,但还没有求出强连通分量的点
bool instk[MAXN];
int cnt=0;//有几个强连通分量
void add_edge(int s,int e){//添加一个从s出发到e的长度为d的边
//push_back:向vector的末尾加入一个元素
z[s].push_back(e);
}
void tarjan(int i){//当前搜索到了i点
num++;
dfn[i]=low[i]=num;
s.push(i);
instk[i]=true;
for(int k=0;k<z[i].size();k++){
int j=z[i][k];
if(!dfn[j]){//树边
tarjan(j);
low[i]=min(low[i],low[j]);
}
else{//非树边
//这是一条回边 或者 能扩大环的横叉边
if(instk[j])low[i]=min(low[i],dfn[j]);
//if(instk[i])low[i]=min(low[i],low[j]);
}
}
if(dfn[i]==low[i]){
cnt++;//多了一个强联通分量
while(s.top()!=i){
belong[s.top()]=cnt;
instk[s.top()]=false;
s.pop();
}
s.pop();
instk[i]=false;
belong[i]=cnt;
}
}
Day6上午 数学
取模运算基础
要算
错误做法:
#include<bits/stdc++.h>
using namespace std;
signed main(){
int ans=1;
int n,p;
cin>>n>>p;
for(int i=1;i<=n;i++)ans*=i;
cout<<ans%p;
return 0;
}
如果算完阶乘再取模很可能爆掉,得出的结果就是错的。
正确做法:
边乘边模
#include<bits/stdc++.h>
using namespace std;
signed main(){
int ans=1;
int n,p;
cin>>n>>p;
for(int i=1;i<=n;i++)ans=ans*i%p;
cout<<ans;
return 0;
}
快速幂
求
快速幂在算法竞赛中有多种理解方式,接下来介绍两种较好理解的。
理解方式一:
我们在六年级的时候学过同底数幂的乘法法则,得知
int ksm(int x,int y,int p){
if(y==0)return 1;
int z=ksm(x,y/2,p);
z=1ll*z*z%p;
if(y%2)z=1ll*z*x%p;
return z;
}
理解方式二:
根据数学常识,每一个正整数可以表示为若干指数不重复的
因为 b&1
运算可以取出 b>>1
运算可以舍去最低位,在递推的过程中将二者结合,就可以遍历
int power(int a,int b,int p){
int ans=1%p;
for(;b;b>>=1){
if(b&1)ans=(long long)ans*a%p;
a=(long long)a*a%p;
}
return ans;
}
我们通过“右移运算”和“与运算”的结合,遍历了
逆元
若
费马小定理:
两边同时
我们发现
所以
所以,在
欧拉定理
考虑
我们发现如果一个数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)