zxs66的日记
31天复习计划
20240312 31
二分
感觉二分很不扎实,好好复习。
整数二分
# P2249 【深基13.例1】查找
问题:我写的二分答案是搜到小于x的最后一个位置,但是实际含义是大于等于x的第一个位置。
原因:二分答案边界确实是搜到小于x的最后一个位置结束,但是我的ans只记录大于等于x的位置,所以最后一个被记录答案的一定是大于等于x的第一个位置
84 第一个点wa了;
出错的原因:没有认真读题 "非负整数"!表示可能存在 0 。
然而,发现改改边界,可以忽略这个情况
l=1,r=n;
不从 开始,开始使用 的原因是:认为是搜到小于 x 的最后一个位置。如果 1 是答案,那么搜到的位置应该是 ,所以在边界往前靠,但是因为实际操作是错的,所以此时 是没有用的。
code
实数二分
实数二分需要注意的点:精度的问题。
写出以下几点:
if(r-l>eps)
中等号写不写都一样- 精度问题,使用
long double
,注意输出方式printf("%.2Lf",x)
- 关于记录答案的操作,感觉和整数二分是一样的
例题
P1542 包裹快递
看似是一个绿题,实际是到水题,起码水 90不成问题,最后一个点出题人确实卡精度很厉害,这也让我知道对于数据很大的实数二分如何正确最对。
卡精度
在这道题目中卡精度的意思是指:在 check
的时候由于数据范围超过了 double 的最大值,注意是最大值,导致爆掉了
long double 数据范围 :18~19位
1.210-4932~1.210+4932
还有这句话
double tim=1.0*a[i].s/(1.0*x);
将 double 改成 long double 会报错。
关于 long double 的输出:
printf("%.2Lf",x);
因为这一点,确实可以作为一个绿题出现,但是属于绿题里面比较 low 的题目。
code
三分法
虽然是以前的笔记,但是我依然选择用以前的写法。
简单说一下现在的我的三分出现的问题:
- 关于三分极值的原理是搞明白的,就是如何缩短区间的原理。
- 但是以前写法和现在书上写法不同的是,三等分的理解。
原来代码的写法是二分以后再二分,
而现在书上说的是三等分线。
我去搜了搜资料,我竟然没有找到双重二分的博客,都是一些奇怪的东西,无奈,我没办法找出差别。
但是我就是三等分线写不出来,双重二分就能写对。
所以我选的双重二分。
code
离散化
学习书上的离散化,觉得比较好理解
void discrete()
{
sort(a+1,a+1+n);
for (int i=1;i<=n;i++)
if (i==1||a[i]!=a[i-1]) b[++num]=a[i];
}
例题
火烧赤壁
这道题目很显然是一道左右区间排序的问题,处理一下边界问题,就可以做了,但是,这里并没有用到离散化,但是这道题目用离散化。
第一种做法:
存在的问题:在处理边界的时候,会存在相同的区间若干,在统计答案时,也就是下面
if (a[i].l>(r-1))//没有考虑到 0 1 ,0 1 的情况
{
sum+=r-l;
l=a[i].l;
r=a[i].r;
}
if 的等于不可以加上,否则会重复计算,这是一种 0,1 这一种特殊情况下,因为题目还有一个性质是 左闭右开,所以这种特殊情况卡的死死的,
只有 80pts
第二种做法
这才是这道题目的正菜
离散化+差分 天生一对
这道题目可以简化为:黑色刷漆,区间刷,求刷的长度。
有一种想法是看成区间加,这不难想到差分,但是发现统计答案的时候不好搞。
我们将问题反过来想,请问答案一定是一个区间,左右边界的边界都是0,中间不是0,对吧,
那么左右边界是不是给出的左右端点的其中两个,可能两个左端点,可能不是原来一对的端点。
那么这说明,给定的左右端点不是绝对的二元关系。
我只需要找出两个区间端点,之间不为零就可以了,
将区间问题拆成差分看的话,就是两个独立问题,一个是后缀加,一个是后缀减,
通过求前缀和来了解当前边界是否为零,
如果知道前缀和,找到连续不为零区间就简单了,只需要用l,r作为连续区间的左右边界即可。
现在来解决前缀和的问题,这其实就是把两个边界当成两个点,区间加而已,但是我们发现,每个区间端点之间差的很大,不容易遍历,这就想到离散化。
所以离散化每个区间端点,问题就解决了。
哎,说的挺啰嗦的。
为什么说天生一对:
因为离散化,使得区间端点可以按顺序遍历,从而可以找到连续有值区间,这样就把重叠区间相当于缩小求解。
也就是说,因为离散化,将区间重叠问题转化为单点问题,这里面其实还有差分的原因,
对于区间,我们利用差分思想,可以将区间问题转化成一个后缀加,一个后缀减,然后就变成两个独立的子问题了。
这么看,二者都是联系在一起,有点优美。算是一种模型吧
说完原理,值得注意的是边界问题,因为差分作用,即后缀的影响,前边界0到第一个边界中间的值是0 ,但是最后一个边界到右边界 0 的值不是0,而是最后一个右边界的值,这在求边界长度的时候,最右边界是右边界0,但是不包括。
哎我咋说的这么麻烦,服了!
总结
自己的做题效率很低,阐述问题的能力不强,说的有点啰嗦,累了,明天再干!
20240313 30
今天课比较多,晚上再加上训练,只能最后稍微复习一下
ST 表
发现以前没有写笔记,这里就简单写一下,以防以后再忘。
表示从 开始,往后的 的元素的最大值,转移的话也不难想到,稍微注意边界就可,因为也包含 ,所以 直接作为右区间的左端点。所以转移有
关于查询
因为区间可能不会正好是 ,所以用分区间重叠的方式求最大值。其实就是跳 ,边界也是比较好想的。
那么答案:
其中 就是对应区间长度的 2 的次幂的更小一次幂,有点绕。
关于s的求解
预处理当区间长度为 时,对应的次幂 ,处理方式也是很巧妙,如下
logg[0]=-1;
for (int i=1;i<=n;i++)
logg[i]=logg[i>>1]+1;
好处在于奇数的处理,注意 表示并不是 ,而是 ,所以 logg[0]=-1
其他就没有什么注意点,ST表大概就这些
模板
#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[1000009];
int logg[100009];//logg[i] 表示 2^(i-1) 用来判断区间长度的2的次幂数
int f[100009][22];
int read(){int x;scanf("%d",&x);return x;}
int main()
{
cin>>n>>m;
logg[0]=-1; //注意和上面一样是:实际表示的是 2^(i-1)
for (int i=1;i<=n;i++)
{
f[i][0]=read();
logg[i]=logg[i>>1]+1;
}
for (int j=1;j<=20;j++)
for (int i=1;i+(1<<j)-1<=n;i++)
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
while (m--)
{
int l=read(),r=read();
int s=logg[r-l+1];
printf("%d\n",max(f[l][s],f[r-(1<<s)+1][s]));//将区间取半的原因是方便区间覆盖
}
return 0;
}
总结
今天学代码的时间确实很少,但是还是复习了一部分,进度更进一步,加油喽~
20240314 29
感觉最短路板子还是不够熟,再写一遍
dij 统一的地方:
- 判断是否已经确定最短路,统一写在前面,后边入队的时候就不用判断了
- 重载运算符别忘记写
return
dij模板
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int B=2e5+10;
int head[B],cnt;
struct node
{
int v,nxt,w;
}e[B<<1];
void modify(int u,int v,int w)
{
e[++cnt].nxt=head[u];
e[cnt].v=v;
e[cnt].w=w;
head[u]=cnt;
}
struct node1
{
int u,dis;
bool operator<(const node1 &x)const
{
return dis>x.dis;
}
};
priority_queue<node1>q;
int dis[B];
int vis[B];
int s;
int n,m;
void dij()
{
for (int i=1;i<=n;i++) dis[i]=0x3f3f3f3f;
dis[s]=0;
q.push({s,0});
while (!q.empty())
{
node1 x=q.top();
q.pop();
int u=x.u;
if (vis[u]) continue;
vis[u]=1;
for (int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if (dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
q.push({v,dis[v]});
}
}
}
}
int read(){int x;scanf("%lld",&x);return x;}
signed main()
{
cin>>n>>m>>s;
for (int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
modify(u,v,w);
}
dij();
for (int i=1;i<=n;i++) cout<<dis[i]<<" ";
return 0;
}
倍增求LCA
和 ST 没区别,直接放板子。
以前反的错误都还记忆深刻,都写上了
#include<bits/stdc++.h>
using namespace std;
const int B=5e5+9;
int head[B],cnt;
struct node
{
int v,nxt;
}e[B<<1];
void modify(int u,int v)
{
e[++cnt].nxt=head[u];
e[cnt].v=v;
head[u]=cnt;
}
int n,m,s;
int dep[B],f[B][23];
void dfs(int u,int pre)
{
dep[u]=dep[pre]+1;
for (int i=1;(1<<i)<=dep[u];i++) f[u][i]=f[f[u][i-1]][i-1];//预处理,相当于RMQ
for (int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if (v==pre) continue;
f[v][0]=u;
dfs(v,u);
}
}
int lca(int x,int y)//让深度大的往上跳
{
if (dep[x]<dep[y]) swap(x,y);
for (int i=20;i>=0;i--)
{
if (dep[f[x][i]]>=dep[y])//注意这里等于,只有这样最后二者才是同深度进入
x=f[x][i];
}
if (x==y) return x;
for (int i=20;i>=0;i--)
{
if (f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];//因为在不相等的时候才更新,所以最后需要往上在跳一步。
}
int read(){int x;scanf("%d",&x);return x;}
int main()
{
cin>>n>>m>>s;
for (int i=1;i<n;i++)
{
int u=read(),v=read();
modify(u,v);
modify(v,u);
}
dfs(s,0);
while (m--)
{
int a=read(),b=read();
int ans=lca(a,b);
cout<<ans<<endl;
}
return 0;
}
floyed最短路
之前写的博客 关于 floyd 中为什么 k 放在最外层的个人看法
写写出现的问题
- 在无法到达的判断中出现问题,如下
是正确的,但是改成if(f[s][i]>=0x3f3f3f3f) cout<<(1<<31)-1;
==
就是错的。但是我在想,不可能有比初始最大值大的,而且就算是数据很大,导致大于了初值,这个时候判断称无法抵达也是不对的,所以这里很迷
板子
#include<bits/stdc++.h>
#define int long long
using namespace std;
int f[1009][1009];
int n,m,s;
int read(){int x;scanf("%lld",&x);return x;}
signed main()
{
cin>>n>>m>>s;
memset(f,0x3f3f3f3f,sizeof(f));
for (int i=1;i<=n;i++) f[i][i]=0;
for (int i=1;i<=m;i++)
{
int u=read(),v=read(),w=read();
f[u][v]=min(f[u][v],w);
}
for (int k=1;k<=n;k++)
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
for (int i=1;i<=n;i++)
{
if (f[s][i]>=0x3f3f3f3f)
{
cout<<(1<<31)-1;
}
else cout<<f[s][i];
cout<<" ";
}
return 0;
}
SPFA
怕手生,再写一遍。
vis 的含义很容易和 dij 的混掉。
这里的 vis 表示是否在队列里面,因为无论一个点加入队列多少次,都是用当前最新的 dis[u] 去更新,所以,多次加入只有一次有用,后面的会增加时间复杂度,所以用vis 来判断
而dij的vis是用来判断当前最短路径是否已经更新完它能更新的点,如果已经更新完成,则不需要再加入队列了。
#include<bits/stdc++.h>
using namespace std;
const int B=5e5+5;
int head[B],cnt;
struct node
{
int v,nxt,w;
}e[B<<1];
void modify(int u,int v,int w)
{
e[++cnt].nxt=head[u];
e[cnt].v=v;
e[cnt].w=w;
head[u]=cnt;
}
int n,m,s;
int dis[B];
int vis[B];
void spfa()
{
queue<int>q;
for (int i=1;i<=n;i++) dis[i]=0x3f3f3f3f;
dis[s]=0;
vis[s]=1;
q.push(s);
while (!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for (int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if (dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
if (!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
}
int main()
{
cin>>n>>m>>s;
for (int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
modify(u,v,w);
}
spfa();
for (int i=1;i<=n;i++) cout<<dis[i]<<" ";
return 0;
}
库鲁斯最小生成树
原理:最短边一定是最小生树里面
推论:连接最小森林的边一定是最短边
- 将边排序
- 找到最小边,用并查集合并
思路清晰,简单明了,没有什么值得特别注意的地方
/*
原理:最短边一定是最小生树里面
推论:连接最小森林的边一定是最短边
1.将边排序
2.找到最小边,用并查集合并
*/
#include<bits/stdc++.h>
using namespace std;
const int B=2e5+10;
struct node
{
int u,v,w;
}e[B<<1];
int cmp(node a,node b)
{
return a.w<b.w;
}
int fa[B];
int find(int x)
{
if (fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
int n,m;
int siz[B];
int read(){int x;scanf("%d",&x);return x;}
int main()
{
cin>>n>>m;
for (int i=1;i<=n;i++) fa[i]=i,siz[i]=1;
for (int i=1;i<=m;i++)
{
e[i]={read(),read(),read()};
}
int sum=0;
sort(e+1,e+1+m,cmp);
for (int i=1;i<=m;i++)
{
int u=find(e[i].u);
int v=find(e[i].v);
if (u==v) continue;
fa[v]=u;
siz[u]+=siz[v];
sum+=e[i].w;
}
int x=1;
int y=find(x);
if (siz[y]!=n) cout<<"orz";
else cout<<sum;
return 0;
}
/*
4 2
1 4 3
2 3 4
*/
例题
这道题目表明:库鲁斯卡尔跑最大生成树的原理和最小生成树是一样的。
总结
今天完成了 :LCA,最短路,倍增RMQ,最小生成树的复习,总体来说板子掌握的还是不错的,目前还有树状数组,线段树,和DP,单调队列需要复习。
顺便记录一下,今天教练谴责我们都不刷题,哎,确实挺忙的,现在是第二天的 1:14,宿舍只有我一个人还在奋斗,哈哈哈,还挺上头,加油加油!
20240315 28
树状数组
这篇博文的图非常容易理解。
我来解释一下树状数组的原理
- 利用 lowbit 将一个数列进行分级,形成不同层级,上层级包含下层级,这样就形成了一个树形结构,看博客的图
- 还有一个很优美的性质:当求从 的和的时候,从 开始,所处层级会依次递增,层级越高,覆盖的数也就越多,这就是时间复杂度很短的原因,由于是二进制问题,总能全部覆盖前面的所有数
这里只是简单的说一下,详细还是还是需要看上面的博客
树状数组可以实现的操作
单点修改,区间求和
单点修改
就需要让他所属的更高的层级都发生更新。所以有这个操作
int lowbit(int x){return x&(-x);}
void modify(int x,int y){for (int i=x;i<=n;i+=lowbit(i)) t[i]+=y;}
区间求和
上面说到,可以求解 的区间和,所以直接利用前缀和思想求解就好了
int query(int x){int res=0;for (int i=x;i;i-=lowbit(i)) res+=t[i];return res;}
void work(int l,int r)
{
cout<<query(r)-query(l-1)<<endl;
}
区间修改单点查询
利用差分,区间修改变成单点修改,单点查询变成求前缀和
这里说一下关于差分数组的构造问题,防止自己以后忘记。
无论是否有原数组,不在使用前后相减的问题,而是直接用单点修改操作来进行
void add(int l,int r,int k){modify(l,k);modify(r+1,-k);}
其他和上边一样。
杂题
出现的问题
i+(1<<j)-1<=n
忘写,导致数组越界。
for (int j=1;j<=20;j++)
for (int i=1;i+(1<<(j))-1<=n;i++)
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
f[r-(1<<s)+1][s]
忘记写 1<<s
写成 s
导致答案出现问题
printf("%lld\n",max(f[l][s],f[r-(1<<s)+1][s]));
总结
今天完成了 :LCA,最短路,倍增RMQ,最小生成树的复习,总体来说板子掌握的还是不错的,目前还有树状数组,线段树,和DP,单调队列需要复习。
顺便记录一下,今天教练谴责我们都不刷题,哎,确实挺忙的,现在是第二天的 1:14,宿舍只有我一个人还在奋斗,哈哈哈,还挺上头,加油加油!
20240319 24
01 背包
状态简单,不多说,说说滚动数组优化,和缩短循环的小细节
写写状态转移,写多了滚动数组优化,很容易忘记,当不选的时候的转移,
for (int i=1;i<=n;i++)
for (int j=w[i];j<=m;j++)
f[i][j]=max(f[i][j-w[i]]+v[i],f[i-1][j];
这里忽略了 当 的情况,没有吧f[i-1]的状态完全转移过来。
应该写
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
if (j>=w[i]) f[i][j]=max(f[i][j-w[i]]+v[i],f[i-1][j];
else f[i][j]=f[i-1][j];
滚动数组优化,因为滚动数组之后,需要利用上一次状态的较小的值,所以要保证小值是上一次状态,则应该倒序更新。
模板
for (int i=1;i<=m;i++)
for (int j=n;j>=w[i];j--)
{
f[j]=max(f[j-w[i]]+v[i],f[j]);
}
完全背包
不同 每个物品可以选无数次。
接着滚动数组优化后的01背包,因为可以无限选择,那么可以存在当前 i 层状态的某个 j 去更新某个 k,所以只需要把倒序改成倒序即可
模板
for (int i=1;i<=m;i++)
for (int j=w[i];j<=n;j++)
{
f[j]=max(f[j-w[i]]+v[i],f[j]);
}
天梯赛预备赛补题
K
题目大意:一个 n 个点 m 边的图,每条边有两个边权,w,v,对于 w ,一颗生成树的价值取决于生成树边里 w 的最小值,求在 生成树 w 最大情况下,总和 v 最小。
思路
不难发现,求解生成树 w 的价值最大不难求,只需要排序倒序,就可以求出能使图联通的情况下,最小值最大的那条边,
比赛的时候,我误认为将 当 w 相等的时候,c 按照从小到大排序就能做,然而,显然存在 w 很大,但是c也很大,我没有必要选,其实对于 找到的最小值 w 更大的值来说,他们完全不需要按照最大生成树那样选,因为库鲁斯卡尔的做法是用来求和的,所以从最大的依次选大概率是错误的,
正解做法:将 w 小于 找到的最小值的都删除,在剩余变里面,跑最小生成树。
这也启发我:当遇见最值生成树的时候,要考虑通过删边来解决问题
/*
盲猜是一个有点小脑筋的最小生成树
*/
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int B=2e5+10;
struct node
{
int u,v,w,c;
}e[B<<1],e2[B<<1];
int cmp(node a,node b)
{
return a.w>b.w;
}
int b[10000009];
int cmp2(node a,node b)
{
return a.c<b.c;
}
int num;
int n,m;
int fa[B];
int find(int x)
{
if (fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
int read(){int x;scanf("%lld",&x);return x;}
signed main()
{
n=read(),m=read();
for (int i=1;i<=n;i++) fa[i]=i;
for (int i=1;i<=m;i++)
{
e[i]={read(),read(),read(),read()};
}
int ans1=0x3f3f3f3f,ans2=0;
sort(e+1,e+1+m,cmp);
for (int i=1;i<=m;i++)
{
int u=find(e[i].u);
int v=find(e[i].v);
if (u==v) continue;
fa[v]=u;
ans1=min(ans1,e[i].w);
}
for (int i=1;i<=n;i++) fa[i]=i;
sort(e+1,e+1+m,cmp2);
for (int i=1;i<=m;i++)
{
if (e[i].w<ans1) continue;
int u=find(e[i].u);
int v=find(e[i].v);
if (u==v) continue;
fa[v]=u;
ans2+=e[i].c;
}
cout<<ans1<<endl<<ans2;
return 0;
}
总结
现在是1:09,说实话,最近压力挺大的,天梯不让去,国护还被刷下来,国护训练完就晚上11点了,因为杯刷下来,心情不好,有锻炼了一个小时,想哭但是觉得不应该哭,去找别的方式去发泄,12点我才坐下来开始写代码,今天给自己的标准就是写三道题目,两道DP,一道最小生成,我是怕 TK 把我又从ACM上刷下来,哎,额头上也长压力痘了.....
哎,我的压力好大,压力到没话可说,我还有另一半需要顾忌,真的好累,好像哭..........!
每一件都是我爱的事,每一件又是我必须做的事,我会把他们做好,永不言弃,信守承诺!
本文作者:zxsoul
本文链接:https://www.cnblogs.com/zxsoul/p/18069268
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2021-03-12 倍增从入门到入土