算法基础Part.3

前缀和与差分

建议数组以1开始,这样方便

例. AcWing99 激光炸弹

地图上有 $N$ 个目标,用整数 $X_{i}, Y_{i}$ 表示目标在地图上的位置,每个目标都有一个价值 $W_i$。

注意:不同目标可能在同一位置。

现在有一种新型的激光炸弹,可以摧毁一个包含 $R \times R$ 个位置的正方形内的所有目标。

激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 $x,y$ 轴平行。

求一颗炸弹最多能炸掉地图上总价值为多少的目标。

输入格式

第一行输入正整数 $N$ 和 $R$,分别代表地图上的目标数目和正方形的边长,数据用空格隔开。

接下来 $N$ 行,每行输入一组数据,每组数据包括三个整数 $X_{i}, Y_{i}, W_{i}$,分别代表目标的 $x$ 坐标,$y$ 坐标和价值,数据用空格隔开。

输出格式

输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。

数据范围

$0 \le R \le 10^9$
$0 < N \le 10000$,
$0 \le X_{i}, Y_{i} \le 5000$
$0 \le W_i \le 1000$

输入样例:

2 1
0 0 1
1 1 1

输出样例:

1

题解

#include <iostream>

using namespace std;

const int N = 5010;

int g[N][N];

int main()
{
    int N, r;
    cin >> N >> r;
    int n = r, m = r;
    for (int i = 0, x, y, w; i < N; i++)
    {
        cin >> x >> y >> w;
        x++, y++;//写前缀和时坐标从1开始可以不用处理边界问题
        n = max(n, x); m = max(m, y);//得到区域的下边界和右边界
        g[x][y] += w;
    }

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            g[i][j] = g[i][j] + g[i - 1][j] + g[i][j - 1] - g[i - 1][j - 1];//动态规划的思想,让g直接变成前缀和数组
        }//这一行的具体实现是在大脑中想想一张图,把每个点的w变为每个点左上方w总和
    }

    int res = 0;
    for (int i = r; i <= n; i++)
    {
        for (int j = r; j <= m; j++)
        {
            res = max(res, g[i][j] - g[i - r][j] - g[i][j - r] + g[i - r][j - r]);
        }//再根据二维前缀和暴力计算题目所要求的R*R最大值,!多用max和min,方便
    }

    cout << res << endl;

    return 0;
}

例. AcWing100 增减序列

给定一个长度为 $n$ 的数列 ${a_1,a_2,…,a_n}$,每次可以选择一个区间 $[l,r]$,使下标在这个区间内的数都加一或者都减一。

求至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列可能有多少种。

输入格式

第一行输入正整数 $n$。

接下来 $n$ 行,每行输入一个整数,第 $i+1$ 行的整数代表 $a_i$。

输出格式

第一行输出最少操作次数。

第二行输出最终能得到多少种结果。

数据范围

$0 < n \le 10^5$,
$0 \le a_i <2147483648$

输入样例

4
1
1
2
2

输出样例

1
2

题解

//这道题用差分,因为我们知道,一个数列,把他每一项视为某个数列的前n项和,即那个数列为差分数列的时候,对差分数列某个数+1,就会让原数列对应数字及之后所有数字都+1,因为+1累积于那个+1,这个时候,通过差分序列有多少正数,有多少负数,我们可以知道它们相对于第一个数字,有多少偏差,每一个正数可以抵消一个等绝对值的负数,所以首先必须要进行min(正数总和,绝对值的负数的总和),然后根据abs(两种数绝对值之差),算出要进行多少次[1,x]或[x,n],的操作,因为对差分数列某个数操作,会让或某个数列之后所有数全部发生相同变化,而某两个数,一加一减,就是l,r那一段全部相同变化,而最少操作总数则取决于abs(两种数绝对值之差)+1是多少,也就是左端或右端整个一段平衡的分配,比如abs=5,则有左端全体操作5次,右端0;1,4;2,3;3,2;4,1;5,0;这一共六种操作。
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

int a[N];

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = n; i > 1; i--) a[i] = a[i] - a[i - 1];

	LL pos = 0, neg = 0;
	for (int i = 2; i <= n; i++)
	{
		if (a[i] > 0) pos += a[i];
		else neg = neg - a[i];
	}

	cout << min(pos, neg) + abs(pos - neg) << endl;
	cout << abs(pos - neg) + 1 << endl;

	return 0;
}

例. AcWing101 最高的牛

有 $N$ 头牛站成一行,被编队为 $1、2、3…N$,每头牛的身高都为整数。

当且仅当两头牛中间的牛身高都比它们矮时,两头牛方可看到对方。

现在,我们只知道其中最高的牛是第 $P$ 头,它的身高是 $H$ ,剩余牛的身高未知。

但是,我们还知道这群牛之中存在着 $M$ 对关系,每对关系都指明了某两头牛 $A$ 和 $B$ 可以相互看见。

求每头牛的身高的最大可能值是多少。

输入格式

第一行输入整数 $N, P, H, M$,数据用空格隔开。

接下来 $M$ 行,每行输出两个整数 $A$ 和 $B$ ,代表牛 $A$ 和牛 $B$ 可以相互看见,数据用空格隔开。

输出格式

一共输出 $N$ 行数据,每行输出一个整数。

第 $i$ 行输出的整数代表第 $i$ 头牛可能的最大身高。

数据范围

$1 \le N \le 10000$,
$1 \le H \le 1000000$,
$1 \le A,B \le 10000$,
$0 \le M \le 10000$

输入样例

9 3 5 5
1 3
5 3
4 3
3 7
9 8

输出样例

5
4
5
3
4
4
5
5
5
注意:
  • 此题中给出的关系对可能存在重复

题解

#include <iostream>
#include <set>

using namespace std;

const int N = 10010;
int height[N];

int main()
{
	int n, p, h, m;
	cin >> n >> p >> h >> m;
	height[1] = h;//这个时候height还是每头牛高度的差分序列,也就是说只要height[1]=h,就相当于所有牛初始值都为1
    //此题有个特性就是所有要求区间不会交叉,因为交叉会有悖论,物理上不可能,无法做到。假设一下就明白
    //所以当区间是嵌套/平行关系的时候,只要先让所有牛是最大值,然后再根据要求让夹在中间的牛身高都减1即可

	set<pair<int, int>> existed;
	for (int i = 0, a, b; i < m; i++)
	{
		cin >> a >> b;
		if (a > b) swap(a, b);
		if (!existed.count({ a,b }))
		{
			existed.insert({ a,b });
			height[a+1]--, height[b]++;//原本差分是a~b+1,然后根据这题实际意义是这两端中间都减1,就是a-1~b,即保证两端都能看见
		}
	}

	for (int i = 1; i <= n; i++)
	{
		height[i] = height[i - 1] + height[i];//将差分序列变为原本真正表示身高的数列,并输出。
		cout << height[i] << endl;
	}

	return 0;
}//差分特点是擅长对一个数列的一部分/一个区间进行整体操作,最后再反映到原数据/数列上。

ios::sync_with_stdio(false);加快cin,但副作用是有的函数无法使用。

关键是Sn-Sn-1=an,Sn=Sn-1+an的使用,以及al+=c ar-=c原数列对整体的影响

双指针算法

核心:将暴力的i、j枚举n2复杂度变为小于n2,比如快排、归并的双指针

步骤

//步骤:
//1、先写出朴素做法
for(int i=0;i<n;i++)
{
    for(int j=0;j<n;j++)
    {
    	if(check(i,j)) 
            do操作
    }
}
//2、根据指针性质改写:双指针算法
for(int i=0,j=0;i<n;i++)//注意,j走过的路,就不在走了,j=0只被赋值了一次,j不会回头
{
    do sth
    while(do sth) do sth j++;//具体题目具体分析
    do操作;
}
//大幅降低时间复杂度

例.AcWing799 最长连续不重复子序列

给定一个长度为 $n$ 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

输入格式

第一行包含整数 $n$。

第二行包含 $n$ 个整数(均在 $0 \sim 10^5$ 范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围

$1 \le n \le 10^5$

输入样例

5
1 2 2 3 5

输出样例

3

题解

#include <iostream>
#include <cstring>
using namespace std;

const int N = 1e5 + 10;
int a[N],s[N];

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);

    int res = 0;
    for (int i = 0,j = 0; i < n; i++)
    {
        s[a[i]]++;
        while (s[a[i]]>1) 
        {
            s[a[j]]--;
            j++;//不断右移j直到区间内重复的一个数字被排除
        }
        res = max(res, i - j + 1);
    }

    printf("%d", res);
    return 0;
}

位运算

lowbit函数

具体写法:
lowbit(x)=x&(-x)=x&(~x+1)波浪号为取反。
作用:
返回x的最后一位1以及之后的0,以二进制形式。

比如lowbit(1010101000)=1000 (这里的数字都是二进制)

简单应用:计算一个数1在哪些位,不断给原数减去lowbit可以遍历每一个1(二进制下)

离散化:将大范围离散数字映射到自然数上,先去重,以下标为映射值,找数字对应的下标就利用二分:值域大数据却稀疏

模板

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素
//unique是将所有重复元素放到数组后方,然后返回第一个重复元素位置的函数
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 映射到1, 2, ...n,若不+1就是从0开始的数组下标
}
//如果大范围数字本身就对应什么小范围数字,对大范围数字用find函数,把对应的数字存到一个新数组里。

例. AcWing802 区间和

假定有一个无限长的数轴,数轴上每个坐标上的数都是 $0$。

现在,我们首先进行 $n$ 次操作,每次操作将某一位置 $x$ 上的数加 $c$。

接下来,进行 $m$ 次询问,每个询问包含两个整数 $l$ 和 $r$,你需要求出在区间 $[l, r]$ 之间的所有数的和。

输入格式

第一行包含两个整数 $n$ 和 $m$。

接下来 $n$ 行,每行包含两个整数 $x$ 和 $c$。

再接下来 $m$ 行,每行包含两个整数 $l$ 和 $r$。

输出格式

共 $m$ 行,每行输出一个询问中所求的区间内数字和。

数据范围

$-10^9 \le x \le 10^9$,
$1 \le n,m \le 10^5$,
$-10^9 \le l \le r \le 10^9$,
$-10000 \le c \le 10000$

输入样例

3 3
1 2
3 6
7 5
1 3
4 6
7 8

输出样例

8
0
5

题解

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

typedef pair<int,int> PII;

const int N = 300010;

int n,m;
int a[N],s[N];

vector<int> alls;
vector<PII> add,query;

int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;//此题用前缀和所以最小从1开始
}

int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});
        
        alls.push_back(x);
    }
    
    for(int i=0;i<m;i++)
    {
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});
        
        alls.push_back(l);//这样做会舒服很多,不用考虑边界问题,直接把这个点加入数轴,就可以很准确的包围起来以后想要的数。
        alls.push_back(r);
    }
    
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(),alls.end()),alls.end());
    
    for(auto item : add)
    {
        int x = find(item.first);
        a[x]+=item.second;
    }
    
    for(int i = 1;i<=alls.size();i++) s[i]=a[i]+s[i-1];
    
    for(auto item : query)
    {
        int l=find(item.first),r=find(item.second);
        cout<<s[r]-s[l-1]<<endl;
    }
    
    return 0;
}

启发

离散化的这种映射为自然数的操作是彻底的,也是双向的,完全用find()和自然数替代了原本的大数字与大数字对应,不仅是将大数字与自然数建立关系,也让大数字对应的(也可以说是键值对的值)数与自然数建立关系(通过find()这套规则以及将题目所给的数据重新再存入一个新的vector),之后便可依靠find完全抛弃大数字,转而直接对新的数组之间操作。还是多做题。

区间和并:以左或右端点排序

1、按照区间左端点排序

2、从左到右扫描区间,选取最左侧区间为标准,与其他区间只有三种情况 a包含 b相交(左端包含) c在右侧

3、遇到c更新区间

详见例题

例.AcWing803 区间和并

给定 $n$ 个区间 $[l_i, r_i]$,要求合并所有有交集的区间。

注意如果在端点处相交,也算有交集。

输出合并完成后的区间个数。

例如:$[1,3]$ 和 $[2,6]$ 可以合并为一个区间 $[1,6]$。

输入格式

第一行包含整数 $n$。

接下来 $n$ 行,每行包含两个整数 $l$ 和 $r$。

输出格式

共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围

$1 \le n \le 100000$,
$-10^9 \le l_i \le r_i \le 10^9$

输入样例

5
1 2
2 4
5 6
7 8
7 9

输出样例

3

题解

#include <iostream>
#include <algorithm>

using namespace std;

typedef pair<int,int> PII;

const int N = 100010;

PII a[N];

int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        int l,r;
        cin>>l>>r;
        a[i]={l,r};
    }
    
    sort(a,a+n);
    
    int res=0,max_r=-2e9;
    for(int i=0;i<n;i++)
    {
        if(max_r<a[i].first)
        {
            res++;
            max_r=a[i].second;
        }
        else max_r=max(max_r,a[i].second);
    }
    
    cout<<res;
    return 0;
}
posted @ 2022-01-02 10:09  泝涉江潭  阅读(105)  评论(0)    收藏  举报