笔记_常见优化技巧2

多次区间修改,单次单点查询——差分数组

修改区间需要的是什么?是确切的改变区间上每一个数据?

不,当区间内的数据变化量相同时,它们间的差值恒定不变,只有区间的开头与结尾区间外的差值发生了变化。我们通过发掘的这“大量的不变”与“少量的变”,省去了大量的无关操作,找到了对付区间修改“特化”的武器

Attention:操作上,对于”区间“的判定

如下图数轴若题目中说需要给在(1,0)和(3,0)之间的方格染黑,你需要用”1“来表示方格染黑,你该怎么维护差分数组?

如下图数轴:

只看题而不画图的话,很多人第一直觉都会觉得差分为a[1]++,a[4]--;但注意了,你的直觉是把它当成了在数组的1~3区间染色,但题目的意思是在坐标轴上1和3之间的区间染色;“区间“二字在坐标轴数组位置上是两个完全不同的概念。我说给数组中b[1]到b[3]区间全加一,自然是1到3包括3的;而在坐标系中,特殊的格子区间,”点中的区间点的数量少一“的幽灵又出来作妖了——(1,0)到(3,0),只有两个区间。所以实际上是a[1]++,a[3]--;

在任何和”网格图“有联系的题目中,都要慎重其中的”区间“概念。

一维差分数组

差分数组存储数据间的差,具有牵一发而动全身的效果。一旦一个位置的差分改变,相当于后续所有数的增减,故我们可以很明显的发现,想要精确的应用差分数据操作某一个区间,在一维上,一定是要同时改变两个位置——

  • 去给予区间影响,并在适当的位置消除影响。

这样才能在影响目标区间的同时,不伤害到其他区间。

二维差分数组

二维差分数组相当于两个一维的组合。从一维组合成二维发生了些许质变,在二维上,差分数组对于数据影响的形式稍稍改变了,其“牵一发而动全身”的效果更为明显了。

于此,在二维平面上,我们需要更多的操作去维护一个精确的矩形区间的修改。

如图,若要让图示”\“格子都进行“+1”操作,则需要对差分数组a进行如图所示的操作:

a[2][2]++, a[2][4]--, a[4][2]--, a[4][4]++;

若差分数组的一维使用相当于从一维线段中“”一段线,那么差分数组的二维使用就是相当于从一个二维的大矩形中精确的“”出一个小矩形。

二维差分数组可以由多种方法实现。实际上,只要你能满足差分的意义,这些方法都能用到二维差分数组的实现上;例如,可以把二维差分数组当作n行的一维差分数组,每次遍历与赋值按n个一维的形式,O(n)的独立(大概率不会和其他操作嵌套,导致时间复杂度相乘)时间复杂度对于优化O(n^2)也已经够用了。

离散化

数据离散化,指的是数据范围很大,但所用到的数据很少,或是并不在意数据本身的具体值,而只在意数据间的相对大小,故而将数据通过映射集中在一个较小的范围;它有些许类似于哈希表,它们同样是需要对数据的区间进行人为的映射与集中。

  • 一维离散化

    一维离散化适用于对于一条很长线段上的少量线段进行操作。

  • 二维离散化数组

    二维离散化数组适用于对于巨大的图上的少量矩形进行操作。

离散化的大致操作:

  1. 录入数据。这一步是为了保存你的原序列。

  2. 重整数据。

    • 重整。把杂乱录入的数据进行排序;这一步是为了把离散化的数据重新集合,我们不需要关注它们在图上的具体方位,我们只用关注这些离散数据的相对方位,即它们间位置/坐标的大小关系。重整的过程中可依照自身方法情况选择是否进行去重。

    • 对应。将排好相对位置关系的数据位置与操作中的真实数据对应出来,一般使用二分法查找位置,或是使用map存储来达到对应的效果。

代码示例__把数组a中的n个数据按照大小映射为1~n:

#include<bits/stdc++.h>
using namespace std;
#define M 500005
int a[M],b[M];
map<int,int> ma;
void Discretization(int a[],int b[])//把a离散化存放在b中 
{
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
    ma[a[i]]=i;//记录排名,等会根据排名进行离散化
    for(int i=1;i<=n;i++)
    b[i]=ma[b[i]];//离散化 依据数组b的排序还原出原序列并进行映射 
}

main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        b[i]=a[i];//b[i]存储a[i]的备份,离散化时使用
    }
    Discretization(a,b);
}

可以看到,离散化有个特点,需要在多个数组间反复捯饬,记录大小排序、记录如何映射、记录初始序列。

特别的,在进行二维离散化数组操作时,除了把两个维度分开拿开进行两次一维的重整,而后再合并之外,还可以直接把两个维度放在一起重整(相当于是二维数组的一维化,不过比这在操作上简单明了不少,但是这样重整出来的图空间利用率有所降低,但是的但是这个”降低“可忽略)

例题:

提示:离散化集中数据后进行染色。但由于是矩形染色,即使离散化之后,一个一个染耗时也长,故可在离散化之后采取差分的形式进行染色。

为了书写方便,推荐采用统一离散(即把两个维度放在一起进行离散)

统一离散听起来很抽象,那到底怎么实现这种看起来玄乎的东西?

一切尽在代码中,多看几遍就理解了。

代码示例:

#include<bits/stdc++.h>
using namespace std;
long retangular[1005][5];//记录n次操作,每次操作都要录入x,y,x1,y1,四个数据
long a[5000],m[5000];//m存放所有的操作点,a用来存放m去重后的数据
int num,nu,k;
long long ans;
map<long,int> ma;//记录每个数值的映射值
int judge[4100][4100];//二维差分数组,用于快速区间染色
main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) //录入数据 
    {
        cin>>retangular[i][1]>>retangular[i][2]>>retangular[i][3]>>retangular[i][4];
        for(int j=1;j<=4;j++)
        m[++nu]=retangular[i][j]; //都存放进m进行统一离散
    }
    //重整数据
    sort(m+1,m+nu+1);//排序
    m[0]=-1e8;
    for(int i=1;i<=nu;i++)//对m进行去重,并存放进a中
    if(m[i]!=m[i-1])
    a[++num]=m[i];
    for(int i=1;i<=num;i++)//记录每个真实值重整后的位置,即映射值
    ma[a[i]]=i;
    //对原序列进行映射 
    for(int i=1;i<=n;i++)
    for(int j=1;j<=4;j++)
    retangular[i][j]=ma[retangular[i][j]];
    //----------------差分数组进行区间染色----------------------
    for(int i=1;i<=n;i++) //枚举操作 
    for(int j=retangular[i][1];j<retangular[i][3];j++)
    judge[j][retangular[i][2]]--,judge[j][retangular[i][4]]++;
    //还记得上文提到的”注意网格图的‘区间’含义“吗?注意差分数组的点别维护错 

    for(int i=1;i<num;i++)
    for(int j=1;j<num;j++)
    judge[i][j]+=judge[i][j-1];//一次性的差分数组,用完了直接还原

    for(int i=1;i<num;i++)
    for(int j=1;j<num;j++)
    if(judge[i][j])
    ans+=(a[i+1]-a[i])*(a[j+1]-a[j]); //展开还原真实距离,得出真实面积
    cout<<ans;
}

分治

区别——分治与递归/递推

有没有常常感觉分治/递推/递归的代码和思路看起来是如此的相像,让人分不清为什么要分出这些名词?

在我的理解中,直观的从代码上区别就是,所谓分治,就是一种为了递推的递归

简单来说,它是以递推为思想纲领,但是是以递推为方法,通过递归来实现的。

分治的核心思想其实并不是传统的”大化小“,而是合并二字;它需要操作的、需要考虑的,只有合并,而并不是”大化小“。

例题

这里的”逆序对“正确方法是通过”归并排序“去找。

归并排序有何特点呢?(以下以快速排序进行对比)

  • 不需要考虑”拆分“的问题,不需要在”拆分“过程中对数组进行操作(如快速排序就需要考虑拆分中的问题)

  • 需要考虑”合并“的问题,需要在”合并“的过程中对数组进行操作(如快速排序就压根不需要合并)

发现了什么?

如果把”快速排序“视为一个经典的”递归“模型,它可以被分解为数个情景完全相同的子问题,且在”出口“处不仅终止递归,还直接完成并结束了运算。

而”归并排序“则是一个经典的”分治“模型,对大问题的分解不是为了一步一步化成小问题并逐个解决(个人理解),而是为了去出口找到那个递推的起点,并且明晰中间合并过程的递推式,最后再逐一返回进行递推。

它采用了递归的方法,确定了递推的步骤

也正因如此,它的“递推”多以“合并”的形式展现

代码示例:

#include<bits/stdc++.h>
using namespace std;
int a[500005];
long long ans;
void together(int l,int r)
{
	if(l==r) return;//到最小子问题了,开始返回合并
	long long mid=(l+r)/2;
	together(l,mid),together(mid+1,r);
	int t[500005];
	int i=l,j=mid+1,k=0;
	while(i<=mid&&j<=r)//合并左右区间
	{
		if(a[i]>a[j])
		{
			t[++k]=a[j++];
			ans+=mid+1-i; 
		}
		else t[++k]=a[i++];
	}
	//合并完成后,把剩余还没排的全部加到后面去 
	if(i>mid)
	{
		while(j<=r)
		t[++k]=a[j++];
	}
	else
    { 
	    t[++k]=a[i++];
	    while(i<=mid)  t[++k]=a[i++];
	}
	for(i=l,k=0;i<=r;i++)
	a[i]=t[++k];
}

main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>a[i];
	together(1,n);
	cout<<ans<<endl;
}

分治(合并)思想在许多方面都能将线性的时间复杂度优化成lnN级别的。

例:

分析:考虑如何分治来求解:
1.如果 p=0,答案就是b^{0}=1 mod k
2. 如果p=2n,即p是一个偶数,那么可以递归求解b^{n}mod k,然后将求得的结果平
方,即b^{n}*b^{n}=b^{2n} = b^{p}
3. 如果p=2n+1,即p是一个奇数,那么可以递归求解b^{n} mod k, 然后将求得的结
果平方,再乘上b,即b^{n}*b^{n}*b=b^{2n+1} =b^{p}

#include<bits/stdc++.h>
using namespace std;
int po(int a,int b)
{
    if(b==0) return 1;
    long sum=1;
    while(b)     //这种算法把任意一个数分为“奇数”与“偶数”两个部分,然后分别各自看这两个部分平方了几次 
    {                 //例如:10 →5*2 →2*2+1 →1*2 →0 可以看到,其中“+1 ”处的那部分共翻倍了一次  
        if(b&1) sum*=a;    //那么从10往下到1时,它只需要乘方一次即可 
        a*=a;                  //递推与递归的两种写法 
        b>>=1;
    }
    return sum;
}

/*void pom(int a) 
{
    int sum=1;
    while(a)
    {
        if(a&1) cout<<"“1”出现的次数:"<<sum<<endl;
        sum++;
        a>>=1;
    }
}*/

main()
 {
 int a,b,c;
 cin>>a>>b>>c;
 cout<<po(a,b)<<endl;
 pom(c);
 }
  • 31
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值