zxs66的日记

31天复习计划

20240312 31

二分
感觉二分很不扎实,好好复习。

整数二分

# P2249 【深基13.例1】查找
问题:我写的二分答案是搜到小于x的最后一个位置,但是实际含义是大于等于x的第一个位置。

原因:二分答案边界确实是搜到小于x的最后一个位置结束,但是我的ans只记录大于等于x的位置,所以最后一个被记录答案的一定是大于等于x的第一个位置

84 第一个点wa了;

出错的原因:没有认真读题 "非负整数"!表示可能存在 0 。

然而,发现改改边界,可以忽略这个情况

l=1,r=n;

不从 \(0\) 开始,开始使用 \(0\) 的原因是:认为是搜到小于 x 的最后一个位置。如果 1 是答案,那么搜到的位置应该是 \(0\),所以在边界往前靠,但是因为实际操作是错的,所以此时 \(0\) 是没有用的。
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

code

第二种做法

这才是这道题目的正菜

离散化+差分 天生一对

这道题目可以简化为:黑色刷漆,区间刷,求刷的长度。

有一种想法是看成区间加,这不难想到差分,但是发现统计答案的时候不好搞。

我们将问题反过来想,请问答案一定是一个区间,左右边界的边界都是0,中间不是0,对吧,
那么左右边界是不是给出的左右端点的其中两个,可能两个左端点,可能不是原来一对的端点。
那么这说明,给定的左右端点不是绝对的二元关系。

我只需要找出两个区间端点,之间不为零就可以了,
将区间问题拆成差分看的话,就是两个独立问题,一个是后缀加,一个是后缀减,
通过求前缀和来了解当前边界是否为零,
如果知道前缀和,找到连续不为零区间就简单了,只需要用l,r作为连续区间的左右边界即可。

现在来解决前缀和的问题,这其实就是把两个边界当成两个点,区间加而已,但是我们发现,每个区间端点之间差的很大,不容易遍历,这就想到离散化。
所以离散化每个区间端点,问题就解决了。

哎,说的挺啰嗦的。

为什么说天生一对:
因为离散化,使得区间端点可以按顺序遍历,从而可以找到连续有值区间,这样就把重叠区间相当于缩小求解。
也就是说,因为离散化,将区间重叠问题转化为单点问题,这里面其实还有差分的原因,
对于区间,我们利用差分思想,可以将区间问题转化成一个后缀加,一个后缀减,然后就变成两个独立的子问题了。
这么看,二者都是联系在一起,有点优美。算是一种模型吧

说完原理,值得注意的是边界问题,因为差分作用,即后缀的影响,前边界0到第一个边界中间的值是0 ,但是最后一个边界到右边界 0 的值不是0,而是最后一个右边界的值,这在求边界长度的时候,最右边界是右边界0,但是不包括。

哎我咋说的这么麻烦,服了!

code

总结

自己的做题效率很低,阐述问题的能力不强,说的有点啰嗦,累了,明天再干!

20240313 30

今天课比较多,晚上再加上训练,只能最后稍微复习一下

ST 表

发现以前没有写笔记,这里就简单写一下,以防以后再忘。

\(f[i][j]\) 表示从 \(i\) 开始,往后的 \(2^j\) 的元素的最大值,转移的话也不难想到,稍微注意边界就可,因为也包含 \(i\) ,所以 \(i+(1<<(j-1))\) 直接作为右区间的左端点。所以转移有

\[f[i][j]=\max\{f[i][j-1],f[i+2^{j-1}][j-1]\} \]

关于查询

因为区间可能不会正好是 \(2^j\) ,所以用分区间重叠的方式求最大值。其实就是跳 \(2^{j-1}\),边界也是比较好想的。
那么答案:

\[\max\{f[l][s],f[r-2^s+1][s]\} \]

其中 \(s\) 就是对应区间长度的 2 的次幂的更小一次幂,有点绕。

关于s的求解
预处理当区间长度为 \(len\) 时,对应的次幂 \(j\),处理方式也是很巧妙,如下

logg[0]=-1;
for (int i=1;i<=n;i++)
	logg[i]=logg[i>>1]+1;

好处在于奇数的处理,注意 \(logg[i]\) 表示并不是 \(2^{i}\) ,而是 \(2^{i-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. 找到最小边,用并查集合并

思路清晰,简单明了,没有什么值得特别注意的地方

/*
	原理:最短边一定是最小生树里面
	推论:连接最小森林的边一定是最短边 
	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
*/

例题

15810: sunflower

这道题目表明:库鲁斯卡尔跑最大生成树的原理和最小生成树是一样的。

code

总结

今天完成了 :LCA,最短路,倍增RMQ,最小生成树的复习,总体来说板子掌握的还是不错的,目前还有树状数组,线段树,和DP,单调队列需要复习。

顺便记录一下,今天教练谴责我们都不刷题,哎,确实挺忙的,现在是第二天的 1:14,宿舍只有我一个人还在奋斗,哈哈哈,还挺上头,加油加油!

20240315 28

树状数组

一篇不错的博文(图很重要)

这篇博文的图非常容易理解。
我来解释一下树状数组的原理

  • 利用 lowbit 将一个数列进行分级,形成不同层级,上层级包含下层级,这样就形成了一个树形结构看博客的图
  • 还有一个很优美的性质:当求从 \([1,x]\) 的和的时候,从 \(x\) 开始,所处层级会依次递增,层级越高,覆盖的数也就越多,这就是时间复杂度很短的原因,由于是二进制问题,总能全部覆盖前面的所有数

这里只是简单的说一下,详细还是还是需要看上面的博客

树状数组可以实现的操作

单点修改,区间求和

单点修改

就需要让他所属的更高的层级都发生更新。所以有这个操作

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;}

区间求和

上面说到,可以求解 \([1,x]\) 的区间和,所以直接利用前缀和思想求解就好了

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);}

其他和上边一样。

杂题

【UPCOJ】【ST表】8600

出现的问题

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];

这里忽略了 当 \(j<w[i]\) 的情况,没有吧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上刷下来,哎,额头上也长压力痘了.....

哎,我的压力好大,压力到没话可说,我还有另一半需要顾忌,真的好累,好像哭..........!

每一件都是我爱的事,每一件又是我必须做的事,我会把他们做好,永不言弃,信守承诺!

posted @ 2024-03-12 20:55  zxsoul  阅读(29)  评论(0编辑  收藏  举报