复习:基础算法

前段时间一直懒得更新,这两天更新一下顺带复习一下
二分啥的其实也应该放进这里面,不过既然已经写过了就算了

前缀和

一维前缀和

若原序列存储在a数组中,则在它的前缀和数组中当下标为i时sum[i]储存的是(a[1]+a[2]+.....+a[i]),即i之前(包括i)的所有元素的和,代码表示为sum[i]=sum[i-1]+a[i]。前缀和的优势在于可以O(1)计算出某个区间的和.
例如要求a[i]到a[j]之间所有数的和,即a[i]+a[i+1]+....+a[j]==sum[j]-sum[i-1];

例:洛谷P8218求区间和

#include<bits/stdc++.h>
using namespace std;
int a[100005];
int main()
{
	int n,m;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		a[i]=a[i-1]+x;
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		printf("%d\n",a[r]-a[l-1]);
	}
	return 0;
}

二位前缀和

和一维前缀和的思想类似但稍微麻烦了一些。由于数组是二维的因此我们也需要放在平面中进行考虑。首先是前缀和的预处理,将二维数组看做一个巨大的长方形,我们将sum[i][j]定义为以长方形左上角为左上顶点,(i,j)为右下顶点的长方形包含的所有店的权值和(可以理解为面积)
二维前缀和的公式是sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];就是一个大长方形减去两个长方形再加上多减去的部分,最后再加上该点的总和。画个图就很简洁了,不过因为我懒,就不在这儿画了。
二维前缀和的用途是可以求数组中任意一块区域的总和。由于任何形状的区域都是可以用长方形拼凑删减得来的,因此我们也只需要知道求一个长方形区域的前缀和公式即可。
假设我们要求一块边长为x的正方形区域的面积,和处理前缀和的时候类似。假设要求区域的右下角坐标为[i][j],则区域面积为sum[i][j]-sum[i-c][j]-sum[i][j-c]-sum[i-c][j]+sum[i-c][j-c];

例:洛谷p2004领地选择

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll a[1505][1505];
int main()
{
	ll inf=1e16;
	ll n,m,c;
	cin>>n>>m>>c;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			ll x;
			scanf("%lld",&x);
			a[i][j]=a[i][j-1]+a[i-1][j]-a[i-1][j-1]+x;
		}
	}
	
	ll maxn=-inf,ansi,ansj;
	for(int i=1;i<=n-c+1;i++)
	{
		for(int j=1;j<=m-c+1;j++)
		{
			int x=i+c-1,y=j+c-1;
			if(x>n) x=n;if(y>m) y=m;
			int c=a[x][y]-a[i-1][y]-a[x][j-1]+a[i-1][j-1];
		//	cout<<c<<' ';
			if(maxn<c) {maxn=c;ansi=i,ansj=j;}
		}
	//	cout<<endl;
	}
	printf("%lld %lld",ansi,ansj);
	return 0;
}

acwing4405统计子矩阵

image
见下方双指针篇的同样题目。复习的时候又做了一遍卡住了,做双指针时当l==r且sum>k时需要特判,不能直接让l++

acwing1230 k倍区间

image
我们需要找到所有的ij组数使得((sum[i]-sum[j-1])%k==0),可以化简为sum[i]%k ==sum[j-1]%k。
也就是说只要这两个值相同就是一种答案
所以我们可以新建一个数组存储sum[i]%k的值,并统计相同值的个数,不难想到对于x个相同的数,取i和j的取法有C(x,2)种,用公式计算就是(x * (x-1))/2.
这里我用unordered_map相同个数(其实可以直接二维数组存)

#include<bits/stdc++.h>
#define int long long
using namespace std;
unordered_map<int,int> q;
const int maxn=1e5+10;
int n,k,ans=0;
int a[maxn],sum[maxn];
signed main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    for(int i=1;i<=n;i++)
    {
        sum[i]=sum[i]%k;
        q[sum[i]]++;
        if(sum[i]==0) ans++;
    }
    for(auto i:q)
    {
        int x=i.second;
       // cout<<x<<' ';
        ans+=x*(x-1)/2;
    }
    cout<<ans;
    return 0;
}

差分

一维差分

差分是前缀和的逆运算,差分数组中c[i]=a[i]-a[i-1].易得a[i]==c[1]+c[2]+....+c[i];
例如一个序列为1 2 3 4 5
那他的差分数组就是1 1 1 1 1
那么我们要这么个数组有什么用呢?
还是上面的数组,我们将c[2]加上1变成2,序列变成1 2 1 1 1
然后我们如果再用求a[i]的公式,会发现这样计算出来的序列是1 3 4 5 6,也就是说第二位以及之后的数组全都加上了1.因此我们可以用差分数组来实现0(1)的局部数组加减。当然如果只有刚才那个操作的话我们只能对数组的某个后缀进行操作,因此我们想要实现对任意区间的修改的话需要抵消前面的影响。对于刚才进行过的操作而言,如果我们只想改变第二个和第三个数字,那我们只需要将c[4]-1即可。
即数组变成了1 2 1 0 1,计算得到的序列为1 3 4 4 5
即若想实现将l到r的区间整体加上x,需要先令c[l]-x,再令c[r+1]+x.

例:ACWing503 借教室

题目大意是一共有n天,并且每天有不同数量的教室空闲,你需要按顺序处理订单将教室借给别人,每份订单都将借走从l天到r天的固定数量的教室。当某天剩余教室不够时无法完成订单,需要取消订单,输出该订单编号即可。
每份订单可以看做对一个区间做减法,很容易想到差分。难点是在对于每个订单减去后,还要查出有没有哪一天的剩余教室是负数。一般在这时候就很容易陷入一个误区,就是觉得逐天检查不现实,因为那样就失去了差分的意义了。这种情况就是默认按顺序每进行一个订单就检查一次了,如果顺着这样想,基本就做不出来了
思路为对答案进行二分,直接判断处理到第x个订单有没有教室不够的情况。因为对于每个订单而言,只有在之前出现过教室不够和没出现过教室不够两种情况。对于每个订单要将它之前的所有订单全都重新再推算一遍,这时候就可以用差分进行0(1)的运算了。然后再O(n)把所有时间扫一遍看看有没有不够的日子。整体复杂度为O(nlogn)。需要注意的是,由于二分时有可能会向左半部分继续二分,因此差分数组要每次都更新

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int maxn=1e6+10;
int val[maxn];
int sum[maxn],cha[maxn];//差分数组以及差分数组前缀和,
int cost[maxn],beg[maxn],endd[maxn];  
int check(int x)//订单x时是否成立
{
	for(int i=1;i<=x;i++)
	{
		//for(int j=1;j<=n;j++) cout<<cha[j]<<' ';
		//cout<<endl;
		cha[beg[i]]-=cost[i];
		cha[endd[i]+1]+=cost[i];
	}
	int flag=0;
	for(int i=1;i<=n;i++)
	{
		sum[i]=sum[i-1]+cha[i];
		cha[i]=val[i]-val[i-1];//初始化差分数组
	//	cout<<sum[i]<<' ';
		if(sum[i]<0) flag=1;
	}
	if(flag) return 0;
	//cout<<endl;
	return 1;
} 
int find(int l,int r)
{
	while(l<r)
	{
	int mid=(l+r)>>1;
	if(check(mid)) l=mid+1;//如果可以说明答案在后面
	else r=mid;
    }
    if(check(l)) return 0;
    else return l;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&val[i]);
		cha[i]=val[i]-val[i-1];
	}
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&cost[i],&beg[i],&endd[i]);
	}
	int ans=find(1,m);
	if(ans) printf("-1\n%d",ans);
	else printf("%d",ans);
	return 0;
}

例:acwing4262 空调

题目大意是给你一个原数组一个目标数组,每次可以对原数组选定一个连续区域进行加一或者减一,问最少几次操作可以得到目标数组。
差分很明显,不过并不是对原数组进行差分,我们要得到目标数组,首先就要知道原数组和目标数组差多少,因此要另外开设一个数组c记录两个数组的差值,并对这个数组c求差分数组。
想让原数组和目标数组完全相同,即两个数组每个位置的数的差值都为0,即我们需要让c数组均变为0.我们对c数组求差分数组。c数组为0,c的差分数组自然也都为0
对于每次操作,我们可以有两种方式:
1.差分数组的第i位加1,第j+1位减1(或者反过来),相当于给原数组的【i,j】区域加一或减一
2.某个值加一或减一,相当于将某个位置一直到最后的所有数都加一或者减一
然后我们想到,1操作可以使差分数组中的正值和负值相互抵消,相当于一次就做了两个2操作。因此如果想最小化操作次数,必须要让正值和负值尽量抵消。不难想到,正数或负数绝对值之和最小一方会被完全抵消,剩下的采用2操作处理,最终操作数量和绝对值之和较大的那一方的绝对值相等。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+20;
int a[maxn],cha[maxn],p[maxn],c[maxn];
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&p[i]);
		c[i]=a[i]-p[i];
	}
	for(int i=1;i<=n;i++) cha[i]=c[i]-c[i-1];
	int sum1=0,sum2=0;
	for(int i=1;i<=n;i++)
	{
		if(cha[i]>0) sum1+=cha[i];
		if(cha[i]<0) sum2-=cha[i];
	 } 
	 printf("%d",max(sum1,sum2));
	return 0;
}

二维差分

和二维前缀和以及一维差分类似,当c[i][j]+v,则以(i,j)为左上端点的无限大的长方形区域都会加上v。如果想给特定区域加上v也和二维前缀和类似。
还是假设要给坐上顶点坐标(i,j)边长为c的正方形整体加v。则需要做以下几个操作
1.c[i][j]+=v;
2.c[i+c][j]-=v;
3.c[i][j+C]-=v;
4.c[i+c][j+c]+=v;
捋清楚其实还蛮简单的
那么如何进行二维差分数组的初始化呢?
image
你用吗?我不用。太麻烦了。
不如直接插入,比如c[1][1]就相当于插入一个左上顶点为(1,1)边长为1的正方形,直接套函数就行。
例题:还没写,未来可能会有

离散化

离散化一般是为了将一组数据范围特别大但是数量不大的数转化成数据范围较小的数。例如如果有1e5个范围-1e9到1e9的不同的数,我们可以通过离散化将他们按照相对大小关系分别表示为1到qe5,这样我们在对序列进行各种操作时会方便许多。
离散化主要是通过二分查找的方式来实现的,我一开始是学的yxc的用vector存的方法,不过后来发现细节太多很容易写错于是现在改成用数组写。
说一下具体实现方式,我们先将原数组a复制一份到另一个数组b,将数组b排序后去重得到新数组,这时候新数组里的数字对应的下标就是它们离散化后的结果。我们想每次找到这个结果,就需要用原来的值通过二分查找来找到它的离散化后下标。

洛谷B3694 数列离散化

这道题就是很简单直白的让你离散化然后输出离散化后坐标,很直白

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int t,n,cnt=0;
const int maxn=1e5+10;
int a[maxn];
int all[maxn];
int find(int x)
{
	int l=1,r=cnt;
	int mid;
	while(l<r)
	{
		mid=(l+r)>>1;
		if(all[mid]>=x) r=mid;
		if(all[mid]<x) l=mid+1;
	}
	return l;
}
int main()
{
	cin>>t;
	while(t--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			all[i]=a[i];
		}
		sort(all+1,all+n+1);
		cnt=unique(all+1,all+n+1)-all;
		for(int i=1;i<=n;i++)
		{
			printf("%d ",find(a[i]));
		}
		printf("\n");
		cnt=0;
	}
	return 0;
}

再贴一下用vector做的,纯纯给自己上难度,用数组的话记得开大点就好了,再也不用vector写离散化了。
这个要注意在去重过后把最后面重复的部分都删去,查找的时候下标从0开始,二分结果要加上1因为a数组下标从一开始。还要注意每次运算完后清空vector数组

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int t,n;
const int maxn=1e5+10;
int a[maxn];
vector<int> all;
int find(int x)
{
	int l=0,r=all.size();
	int mid;
	while(l<r)
	{
		mid=(l+r)>>1;
		if(all[mid]>=x) r=mid;
		if(all[mid]<x) l=mid+1;
	}
	return l+1;
}
int main()
{
	cin>>t;
	while(t--)
	{
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]);
			all.push_back(a[i]);
		}
		sort(all.begin(),all.end());
		all.erase(unique(all.begin(),all.end()),all.end());
		for(int i=1;i<=n;i++)
		{
			printf("%d ",find(a[i]));
		}
		printf("\n");
		all.erase(all.begin(),all.end());
	}
	return 0;
}

高精度

双指针

洛谷p8783统计子矩阵

image
想了挺久没想出来,这一块儿学的还是不太好,要是正式比赛我估计就拿七十分跑路了
比较容易想到的是前缀和+四次方暴力枚举,能过70的点,题目最大数据n和m是500,应该考虑三次方做法。
答案是将二维压缩为一维,每个一维是On,也就是说要跑n方次,就是枚举上下边界。如果枚举上下边界就不需要考虑矩形的宽度,只需要考虑长度即可。On处理需要用到双指针,当lr内的区间小于等于k时更新答案r++,当区间长度只有1时r++,其他情况就l++。对于每次区间值小于k的情况答案增加(r-l+1),原因是宽度固定,只考虑长度的话,对于长度为c的区间,增加了1以后变为c+1,那么增加的答案有:长度为1的矩形一个,长度为2的矩形一个.....长度为c+1的矩形一个,共有(区间长度)个,也就是(r-l+1).
复习的时候又做了一遍卡住了,做双指针时当l==r且sum>k时需要特判,不能直接让l++

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=505;
int n,m,k;
int sum[maxn][maxn];
int he(int i1,int j1,int i2,int j2)
{
	return sum[i2][j2]-sum[i2][j1-1]-sum[i1-1][j2]+sum[i1-1][j1-1];
}
signed main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			int x;
			scanf("%lld",&x);
			sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+x;
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++)//枚举上下边界 
	{
		for(int j=i;j<=n;j++)
		{
			for(int l=1,r=l;r<=m;)
			{
			//	printf("%ld %ld %ld\n",l,r,ans);
				if(he(i,l,j,r)<=k) ans+=(r-l+1),r++;
				else
				{
					if(l==r) r++;
					else l++;
				}
			}
		//	printf("%ld %ld %ld\n",i,j,ans);
		}
	}
	cout<<ans;
	return 0;
}
posted @ 2024-03-03 22:55  miku今天吃什么  阅读(19)  评论(0编辑  收藏  举报