刷题周记(一)——最长连续不重复子序列 、数的三次方根、差分 、前缀和、最大异或对、快速排序、子矩阵的和、差分矩阵、区间和、区间合并、单链表、双链表、模拟栈、模拟队列、单调栈、滑动窗口(单调队列)

————————————————————2020年10月25日(周日)

一、最长连续不重复子序列

首先要明白,子序列不一定要所有元素挨在一起
== 子串当然就是要再在一起啦==,就像一串糖葫芦一样,被在一起了。

但是,这道题所谓的“最长连续子序列”其实是“子串”啦。
题目
思路注释

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
bool s[N];
int cnt = 0, t = 0;

int main(){
        memset(s, 0, sizeof s);
    cin >> n;
    for(int i = 0; i < n; i ++)
        cin >> a[i];
        
    int j = -1, i;
    for(i = 0; i < n; i ++){//i不断往前走
    	//每一轮都要找到从i开始的最长不重复子序列
        // for(int k = 0; k < 20; k ++) cout << s[k] << ' ';
        //     cout << endl;
        if(t > 0)//如果当前有长度,往前走就要长度-1
            t --;  
        while(j + 1 < n && !s[a[j + 1]]){
            s[a[j + 1]] = 1;
            t ++;
            j ++;
        }
            // cout << i << endl;
        s[a[i]] = 0;
        cnt = max(cnt, t);
        // cout << "cnt :" << cnt << endl;
    }
   
    cout << cnt;
    return 0;
}

二、数的三次方根

题目
思路:不断二分找答案,然后记得小数要判断精度!

#include <iostream>
using namespace std;
int main(){
    double x;
    scanf("%lf", &x);
    double l = -100, r = 100;
    while(r - l > 1e-8){
        double mid = (l + r) / 2;
        if(mid * mid * mid >= x) r = mid;
        else l = mid;
    }
    printf("%.6lf", r);
    return 0;
}

三、差分

题目

思路:
将要加上某个数的区间第一个数加上要加的数,
区间末后一个元素减去此元素
就可以使区间内的数都加上此元素而后面的数保持原样

#include<bits/stdc++.h>
#define MAXN 200005
using namespace std;

int n,m,a[MAXN],b[MAXN];

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    int l, r, c;
    for(int i =1 ; i <= m; i ++){
        cin >> l >> r >> c;
        b[l] += c; b[r + 1] -= c;
    }
    for(int i = 1; i <= n ;i ++) b[i] += b[i - 1];
    for(int i = 1; i <= n; i ++) cout << a[i] + b[i] << " ";
    return 0;
}

四、前缀和

转到原题

#include <iostream>

using namespace std;

const int N = 100010;

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

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

    for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i]; // 前缀和的初始化
    while(m --){//这是第一种输入输出方式
        int l, r;
        cin >> l >> r;
        cout << s[r] - s[l - 1] << endl;
    }

    while (m -- )//这是第二种输入输出方式
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d
", s[r] - s[l - 1]); // 区间和的计算
    }


    return 0;
}

两种输入输出时间对比
可以看到时间差了很远很远!!!,所以输入一定要用scanf !

————————————————————2020年10月26日(周一)

一、最大异或对

传送
整理博客时发现的,之前没仔细写思路,现在一开始自己也没有想起来之前怎么做的 (滑稽)

(过了一周后再回来看,发现又忘记怎么做了……我的记忆果然是假的。——一周后的我

注:-1的二进制码全是 1,所以 ~-1 == 0 !

答案

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 3000000;

int n;
int a[N], son[M][2], idx;

void insert (int x){
    int p = 0;
    for(int i = 30; ~i; i --){
        int s = x >> i & 1;
        if(!son[p][s]){
            son[p][s] = ++idx;
            p = idx;
        }
        else p = son[p][s];
    }
}
//这个函数就是说,将这个数分解为一个二进制数,然后按照这个二进制不断地往下开辟一条道路
//每到一个新节点就给它取一个新的名字,这样往下找儿子的时候就不会产生混乱……
int find(int x){
    int p = 0, res = 0;
    for(int i = 30; i >= 0; i --){
        int s = x >> i & 1;
        if(son[p][!s]){
            res += 1 << i;
            p = son[p][!s];
        }
        else p = son[p][s];
    }
    return res;
}

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i ++){
        scanf("%d", &a[i]);
        insert(a[i]);
    }
    
    int res = 0;
    for(int i = 0; i < n; i ++) res = max(res, find(a[i]));
    
    cout << res ;
    return 0;
}

二、快速排序

传送
没啥好说的,对着模板打吧,边界问题一直是个迷……反正也很少用得着手写。(除了练习的时候,谁会手打这玩意儿?)

#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
void quick_sort(int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(l, j);
    quick_sort(j + 1, r);
}
int main(){
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++) scanf("%d ", &q[i]);
    quick_sort(0, n - 1);
    for(int i = 0; i < n; i ++) printf("%d ", q[i]);
    return 0;
}

————————————————————2020年10月27日(周二)

一、子矩阵的和(二维前缀和)


思路:

s[i][j] += ( s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] ) ;
这个是将输入后的数组进行初始化,代表的是以(1, 1)为左上角以(i, j)为右下角的原矩阵的所有元素之和。
s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]
这句话是求规定范围(以(x1, y1)为左上角,以(x2, y2)为右下角)内的矩阵的所有元素之和。

答案

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m, q;
int s[N][N];

int main(){
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            scanf("%d", &s[i][j]);
            
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            s[i][j] += ( s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] ) ;
    
    for(int i = 1; i <= q; i ++){
        int x1, x2, y1, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d
", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }
    return 0;
}

二、差分矩阵

turn
思路
只有一个操作:


void insert(int x1, int y1, int x2, int y2, int c){
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

就是这个insert函数,意思是 将这个矩阵之内的数全部加上c, 其余部分不会变。
而最后的实现其实就是前面子矩阵的和这道题的方法,也就是将最后得到的b数组进行二维前缀和的操作,
得到的b数组就是答案

举例理解:

	首先 假设 有一个4X4 的小方块要加上 2, 那么我们先将方块的左上角(1, 1)加上2。
 	然后你想,我们最后是要进行二维前缀和的运算的,
 	那么为了不让其它部分受到影响,我们要将(1, 5)(5, 1)上的数减去2,与从(1, 1)传递过来的2相互抵消了,这样它们后面的数也不会受到影响了。
 	
	但是注意!有一个位置,那就是b(5, 5)它减去了两次2!
	因为它同时受b(5, 1)b(1, 5)的影响,所以我们还要在b(5, 5)再加上2.

答案

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

void insert(int x1, int y1, int x2, int y2, int c){
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

int main(){
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i ++)
        for(int j = 1;j <= m; j ++){
            scanf("%d", &a[i][j]);
            insert(i, j, i, j, a[i][j]);
        }
        
    for(int i = 1; i <= q; i ++){
        int x1, x2, y1, y2, c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        insert(x1, y1, x2, y2, c);
    }
    
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
            
    for(int i = 1; i <= n; i ++){
        for(int j = 1; j <= m; j ++)
            printf("%d ", b[i][j]);
        puts("");
    }    
    return 0;
}

————————————————————2020年10月28日(周三)

一、区间和

传送
思路
离散化(Discretization):把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。

离散化 —— 模板题 AcWing 802. 区间和
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 二分求出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
}

作者:yxc
链接:https://www.acwing.com/blog/content/277/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

传送
答案
总感觉这种写法太麻烦了 ……

#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> P;
const int N = 300010;
int m, n;
int a[N], s[N];
vector<int> alls;
vector<P> 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;
}
vector<int>::iterator unique(vector<int> &a){
	int j = 0;
	for(int i = 0; i < a.size(); i ++)
		if(!i || a[i] != a[i - 1])
			a[j ++] = a[i];
    // a[0] ~ a[j - 1] 所有a中不重复的数
	return a.begin() + j;
	//返回end坐标
}

int main(){
	scanf("%d%d", &n, &m);
	//"首先进行 n 次操作,每次操作将某一位置x上的数加c。"
	for(int i = 0; i < n; i ++){
		int x, c;
		scanf("%d%d", &x, &c);
		add.push_back({x, c});
		alls.push_back(x);
	}
	//"接下来,进行 m 次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和"
	for(int i = 0; i < m; i ++){
		int l, r;
		scanf("%d%d", &l, &r);
		query.push_back({l, r});
		alls.push_back(l);
		alls.push_back(r);
	}
	//去重
	sort(alls.begin(), alls.end());
	alls.erase(unique(alls), 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] = s[i - 1] + a[i];
		
		// 处理询问
	for(auto item : query){
		int l = find(item.first);
		int r = find(item.second);
		cout << s[r] - s[l - 1] << endl;
	}
	return 0;
}

————————————————————2020年10月30日 (周五)

一、区间合并

传送
思路

区间合并 —— 模板题 AcWing 803. 区间合并
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
    vector<PII> res;

    sort(segs.begin(), segs.end());

    int st = -2e9, ed = -2e9;
    for (auto seg : segs)
        if (ed < seg.first)
        {
            if (st != -2e9) res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else ed = max(ed, seg.second);

    if (st != -2e9) res.push_back({st, ed});

    segs = res;
}

作者:yxc
链接:https://www.acwing.com/blog/content/277/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

答案

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
void merge(vector<PII> &segs){
    vector<PII> res;
    sort(segs.begin(), segs.end());

    int st = -2e9, ed = -2e9;
    for (auto seg : segs)
        if (ed < seg.first){//没有交集
            if (st != -2e9) res.push_back({st, ed});//也不是第一个,那就加入一个新的合成后的区间
            st = seg.first, ed = seg.second;//但是不论如何,st和ed都要重新开始
        }
        else ed = max(ed, seg.second);
    if (st != -2e9) res.push_back({st, ed});//有交集的话,比较一下谁的尾巴更长就用谁的尾巴,这就是合并
    segs = res;
}

int main(){
    int n;
    scanf("%d", &n);

    vector<PII> segs;
    for (int i = 0; i < n; i ++ ){
        int l, r;
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});
    }
    merge(segs);
    cout << segs.size() << endl;
    return 0;
}

————————————————————2020年10月31日(周六)

数据结构模板

一、单链表

传送
模板

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init(){
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a){
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove(){
    head = ne[head];
}

答案
根据题目做了一些小改动,就是把nxt[0]看作是头结点,从而省略了head这个变量以及insert_head这个单独的函数。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int nxt[N], e[N];
int idx = 1;
void add (int n, int a){//在n位置之后加入元素a
    e[idx] = a, nxt[idx] = nxt[n], nxt[n] = idx ++;//注意,与头相反的方向是尾,也就是后面
}
void del(int k){
	nxt[k] = nxt[nxt[k]]; 
}
int main(){
    int m;
    scanf("%d", &m);
    for(int i = 1; i <= m; i ++){
        char c;
        cin >> c;
        if(c == 'I'){
            int k, a;
            scanf("%d%d", &k, &a);
            add(k, a);
        }
        else if(c == 'D'){
            int k;
            scanf("%d", &k);
            del(k);
        }
        else if(c == 'H'){//注意:新插入的点是头
            int a;
            scanf("%d", &a);
            add(0, a);
        }
    }
    for(int i = nxt[0]; i ; i = nxt[i]) printf("%d ", e[i]);
    return 0;
}

二、双链表

传送
这个形象多了,个人觉得更好理解。
在这里插入图片描述

教训:if , else if 和 else一定要区分清楚,不然真的怎么出错都不知道!!

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int l[N], r[N], e[N], idx = 2;

void insert(int k, int x){
    e[idx] = x;
    l[idx] = k, r[idx] = r[k];
    l[r[k]] = idx, r[k] = idx ++;
}
void del(int k){
    l[r[k]] = l[k];
    r[l[k]] = r[k];
}
int main(){
    r[0] = 1, l[1] = 0;
    int m;
    cin >> m;
    for(int i = 1; i <= m; i ++){
        string s;
        cin >> s;
        int k, x;
        if(s == "L"){
            scanf("%d", &x);
            insert(0, x);   
        }
        else if(s == "R"){
            scanf("%d", &x);
            insert(l[1], x);  
        } 
        else if(s == "D"){
            cin >> k;
            del(k + 1);//这里k + 1是因为我们是从2开始计算第一个节点的。  
        } 
        else if(s == "IL"){
            scanf("%d%d", &k, &x);
            insert(l[k + 1], x);    
        }
        else{
            scanf("%d%d", &k, &x);
            insert(k + 1, x);  
        } 
    }
    for(int i = r[0]; i != 1; i = r[i]) printf("%d ", e[i]);
    return 0;
}

三、模拟栈

这种题真的有存在的必要吗……
解答

#include <iostream>
using namespace std;
const int N = 100010;
int m;
int stk[N], tt;
int main()
{
    cin >> m;
    while (m -- )
    {
        string op;
        int x;
        cin >> op;
        if (op == "push")
        {
            cin >> x;
            stk[ ++ tt] = x;
        }
        else if (op == "pop") tt -- ;
        else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
        else cout << stk[tt] << endl;
    }
    return 0;
}

四、模拟队列

这个也是……
解答

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt = -1;

int main()
{
    cin >> m;

    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            q[ ++ tt] = x;
        }
        else if (op == "pop") hh ++ ;
        else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;
        else cout << q[hh] << endl;
    }

    return 0;
}

五、单调栈


思路
这里为什么用单调栈呢?
因为如果输出第一个小于某数的数的话,那么每次一直往左找,直到找到最左边的那个,输出并记录。
如果后来输入的数比它大,那就没有必要保留,类似出栈操作。
这样可以很快地解决这种问题。

#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;
        if (!tt) printf("-1 ");
        else printf("%d ", stk[tt]);
        stk[ ++ tt] = x;
    }

    return 0;
}

六、滑动窗口(单调队列)

传送
思路
经典的单调队列问题,三刷了……
简单地说就死维护一个单调队列,每次输出队尾即可。
梦幻联动(调试版也在里面啦)

#include <iostream>
using namespace std;
const int N = 1000010;
int a[N], q[N];
int main()
{
    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    
//这里处理的是最小值
    int hh = 0, tt = -1//头 和 尾
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt    &&   i - k + 1 > q[hh] )  hh ++ ;
        //这里是当队列里面还有元素 以及 头不在这个窗口范围的时候,头就往后走一位
        while (hh <= tt &&   a[q[tt]] >= a[i]  )  tt -- ;
        //这里是要把队列中所有比 现在要插入的元素 大的元素出队
        //然后就会发现 现在队列里的元素 正是当前状态(加入新元素)的单调队列
        q[ ++ tt] = i;

        if (i >= k - 1) printf("%d ", a[q[hh]])//队头是最小的
    }

    puts("");
    
//这里处理的是最大值
    hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
        q[ ++ tt] = i;

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }

    puts("");
    
    return 0;
}

已完结 (标题满100了塞不下了) ,刚好一周!以后就一周算一章吧!就像日(周)记一样好了……

什么嘛,这周只有16道题而已嘛,平均一天也就两道题多一点……

原文地址:https://www.cnblogs.com/yuanyulin/p/14026729.html