CSP-S算法精选营

数据结构


大根堆

定义:priority_queue<数据类型> 变量名

小根堆

ps:可以使用大根堆数据取相反数,也可使用priority_queue<数据类型,vector<数据类型>,greater<数据类型> >变量名;

#include<queue>
//#include<bits/stdc++.h>

using namespace std;

priority_queue<int> q;
//大根堆 
//小根堆最简单的方法:取负号 

struct rec
{
	int a,b;
};
//如果要把结构体 放入 stl比大小 只能重载小于号 
bool operator<(const rec &x,const rec &y)
{
	return x.a + x.b > y.a + y.b;
}

priority_queue<rec> qq;//取出a+b最小的结构体 

int main()
{
	q.push(233);
	q.push(2233);//向堆里面加入一个元素
	q.pop();//从堆中删除一个元素 删除是堆中最大的元素 2233 void类型没有返回值
	int x = q.top();//获取堆中最大元素 233
	cout << q.size() << endl;//获取堆剩余的元素个数 1 
} 

手写堆

struct heap
{
	int a[1010];//堆的每一个元素 
	int n=0;//堆有几个元素	
	
	int top()//询问最大值 
	{
		return a[1];
	}
	void push(int x)//插入一个数
	{//O(logn)
		n++;a[n] = x;
		int p=n;
		while (p!=1)
		{
			if (a[p] > a[p>>1])
			{
				swap(a[p],a[p>>1]);
				p = p>>1;
			}
			else
			{
				break;
			}
		}
	}
	void pop()//删除最大值
	{
		swap(a[1],a[n]);n--;
		int p=1;
		while ((p<<1) <= n)
		{
			int l=p<<1;
			int r=l|1;//p*2+1
			int pp=l;
			if (r<=n && a[r] > a[l]) pp=r;//pp一定是两个儿子中较大的那个 
			if (a[pp] > a[p])
			{
				swap(a[pp],a[p]);
				p=pp;
			}
			else
			{
				break;
			}
		}
	}
	int size()//询问还有几个数
	{
		return n;
	} 
};

并查集

引入
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

顾名思义,并查集支持两种操作:

合并(Union):合并两个元素所属集合(合并对应的树)
查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合

int to[maxn];//to[i] 代表i的箭头指向谁

int go(int p)//从p点出发 看最后会走到哪里
{
	if (p == to[p]) return p;
	else 
	{
		to[p] = go(to[p]);
		return to[p];
	}
} 

int main()
{
	cin >> n;
	for (int i=1;i<=n;i++)
		to[i] = i;
	
	//合并
	to[go(p1)] = go(p2);
	
	//查询
	go(p1) == go(p2); 
}

优化技巧-按秩合并

int to[maxn];//to[i] 代表i的箭头指向谁

int go(int p)//从p点出发 看最后会走到哪里
{
	if (p == to[p]) return p;
	else 
	{
		to[p] = go(to[p]);
		return to[p];
	}
} 

int main()
{
	cin >> n;
	for (int i=1;i<=n;i++)
		to[i] = i;
	
	//合并
	if (rand()%2)
		to[go(p1)] = go(p2);
	else
		to[go(p2)] = go(p1);
	
	//查询
	go(p1) == go(p2); 
}

Trie树

定义
字典树,英文名 trie。顾名思义,就是一个像字典一样的树

struct node
{
	int nxt[2];//nxt[0] nxt[1] 代表从当前点走0和1会走到哪里 走到0的话代表这个节点不存在 
	node()
	{
		nxt[0] = nxt[1] = 0;
	}
}z[23333];

void insert(int x)
{
	int p=root;
	for (int i=30;i>=0;i--)
	{
		int y=(x>>i)&1;//取出x二进制的第i位 
		if (z[p].nxt[y] == 0) {;
			cnt++;
			z[p].nxt[y] = cnt;
		} 
		p = z[p].nxt[y];
	}
}

int query(int x)//从trie中找一个数 使得他和x异或之后最大 
{
	int p=root,ans=0;
	for (int i=30;i>=0;i--)
	{
		int y=(x>>i)&1;
		if (z[p].nxt[y^1] != 0) ans=ans|(1<<i),p=z[p].nxt[y^1];
		else p=z[p].nxt[y];
	}
	return ans;
}

int main()
{
	root = 1;
}

分块

引入:其实,分块是一种思想,而不是一种数据结构。
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
例:P3372线段树1

#include <cmath>
#include <iostream>
using namespace std;
int id[50005], len;
// id 表示块的编号, len=sqrt(n) , 即上述题解中的s, sqrt的时候时间复杂度最优
long long a[50005], b[50005], s[50005];

// a 数组表示数据数组, b 数组记录每个块的整体赋值情况, 类似于 lazy_tag, s
// 表示块内元素总和
void add(int l, int r, long long x) {  // 区间加法
  int sid = id[l], eid = id[r];
  if (sid == eid) {  // 在一个块中
    for (int i = l; i <= r; i++) a[i] += x, s[sid] += x;
    return;
  }
  for (int i = l; id[i] == sid; i++) a[i] += x, s[sid] += x;
  for (int i = sid + 1; i < eid; i++)
    b[i] += x, s[i] += len * x;  // 更新区间和数组(完整的块)
  for (int i = r; id[i] == eid; i--) a[i] += x, s[eid] += x;
  // 以上两行不完整的块直接简单求和,就OK
}

long long query(int l, int r, long long p) {  // 区间查询
  int sid = id[l], eid = id[r];
  long long ans = 0;
  if (sid == eid) {  // 在一个块里直接暴力求和
    for (int i = l; i <= r; i++) ans = (ans + a[i] + b[sid]) % p;
    return ans;
  }
  for (int i = l; id[i] == sid; i++) ans = (ans + a[i] + b[sid]) % p;
  for (int i = sid + 1; i < eid; i++) ans = (ans + s[i]) % p;
  for (int i = r; id[i] == eid; i--) ans = (ans + a[i] + b[eid]) % p;
  // 和上面的区间修改是一个道理
  return ans;
}

int main() {
  int n;
  cin >> n;
  len = sqrt(n);  // 均值不等式可知复杂度最优为根号n
  for (int i = 1; i <= n; i++) {  // 题面要求
    cin >> a[i];
    id[i] = (i - 1) / len + 1;
    s[id[i]] += a[i];
  }
  for (int i = 1; i <= n; i++) {
    int op, l, r, c;
    cin >> op >> l >> r >> c;
    if (op == 0)
      add(l, r, c);
    else
      cout << query(l, r, c + 1) << endl;
  }
  return 0;
}

莫队

ps:一种基于分块思想的著名离线算法

struct query
{
	int l,r,id,ans;
}q[maxn];

bool cmp1(const query &q1, const query &q2)
{
	if (belong[q1.l] != belong[q2.l]) return belong[q1.l] < belong[q2.l];
	else return q1.r < q2.r;
}

bool cmp2(const query &q1, const query &q2)
{
	return q1.id < q2.id;
}

void ins(int x)
{
	cnt[x] ++;
	if (cnt[x] % 2 == 0) ans++;
	else if (cnt[x] != 1) ans--;
}

void del(int x)
{
	cnt[x] --;
	if (cnt[x] != 0)
	{
		if (cnt[x] % 2 == 0) ans++;
		else ans--;
	}
}

int main()
{
	cin >> n >> m;
	for (int i=1;i<=n;i++)
		cin >> a[i];
	for (int i=1;i<=m;i++)
	{
		cin >> q[i].l >> q[i].r;
		q[i].id = i;
	}
	int s = sqrt(n);
	for (int i=1;i<=n;i++)
		belong[i] = i/s+1;
	sort(q+1,q+m+1,cmp1);	
		
	for (int i=q[1].l;i<=q[1].r;i++)
		ins(a[i]);
	q[1].ans = ans;
	for (int i=2;i<=m;i++)//O(Nsqrt(N))
	{
		int l1=q[i-1].l,r1=q[i-1].r;
		int l2=q[i].l,r2=q[i].r;
		
		if (l1 < l2)
			for (int i=l1;i<l2;i++)
				del(a[i]);
		else
			for (int i=l2;i<l1;i++)
				ins(a[i]);
		
		if (r1 < r2)
			for (int i=r1+1;i<=r2;i++)
				ins(a[i]);
		else
			for (int i=r2+1;i<=r1;i++)
				del(a[i]);

		q[i].ans = ans;
	}
	sort(q+1,q+m+1,cmp2);
	for (int i=1;i<=m;i++)
		cout << q[i].ans << "\n";
		
	return 0;
}

线段树

主要是用于区间问题
形如:

懒标记:对于线段树进行修改时,我们考虑先不对它进行修改,而是对他打上标记后,下一次询问他时在下放到部分里

#include<bits/stdc++.h>

using namespace std;

#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1

const int maxn=100010;

int n,m,a[maxn];

struct node//一个线段树节点 
{
	int sum;//代表区间和
	int size;//代表区间长度 
	int add;//这段区间被整体加了多少
	node()
	{
		sum = size = add = 0;
	} 
	void init(int v)//用一个数初始化 
	{
		sum = v;
		size = 1;
	}
}z[maxn<<2];//z[i]就代表线段树的第i个节点 

node operator+(const node &l,const node &r)
{
	node res;
	
	res.sum = l.sum + r.sum;
	res.size = l.size + r.size;
	
	return res;
}

void color(int l,int r,int rt,int v)//给l,r,rt这个节点打一个+v的懒标记
{
	z[rt].add += v;
	z[rt].sum += z[rt].size * v;
} 

void push_col(int l,int r,int rt)//标记下放 把标记告诉儿子
{
	if (z[rt].add == 0) return; //没标记 不需要下放  可以不要这句话 但会慢些 
	int m=(l+r)>>1;
	color(lson,z[rt].add);
	color(rson,z[rt].add);
	z[rt].add=0;
} 

void build(int l,int r,int rt)//建树 初始化l,r,rt这个节点 
//编号为rt的线段树节点 所对应的区间是l~r 
{
	if (l==r)
	{
		z[rt].init(a[l]);
		return;
	} 
	int m=(l+r) >> 1;
	build(lson);
	build(rson);
	z[rt] = z[rt<<1] + z[rt<<1|1]; 
}

node query(int l,int r,int rt,int nowl,int nowr)
//l,r,rt描述了一个线段树节点
//nowl nowr代表了询问的区间的左端点和右端点 
{
	if (nowl <= l && r <= nowr) return z[rt];
	push_col(l,r,rt);
	int m=(l+r)>>1;
	if (nowl<=m)
	{
		if (m<nowr) return query(lson,nowl,nowr) + query(rson,nowl,nowr);
		else return query(lson,nowl,nowr);
	}
	else return query(rson,nowl,nowr);
}

void modify(int l,int r,int rt,int nowl,int nowr,int v)
//把nowl~nowr这段区间全部整体+v 
{
	if (nowl<=l && r<=nowr)//当前线段树节点被修改区间整体包含
	{
		color(l,r,rt,v);//给l,r,rt这个节点打一个+v的懒标记
		return; 
	} 
	push_col(l,r,rt);
	int m=(l+r)>>1;
	if (nowl<=m) modify(lson,nowl,nowr,v);
	if (m<nowr) modify(rson,nowl,nowr,v);
	z[rt] = z[rt<<1] + z[rt<<1|1];
}

int main()
{
	cin >> n;
	for (int i=1;i<=n;i++)
		cin >> a[i];
	build(root);
	cin >> m;
	for (int i=1;i<=m;i++)
	{
		int opt;
		cin >> opt;
		if (opt==1)//询问
		{
			int l,r;
			cin >> l >> r;
			cout << query(root,l,r).sum << "\n";
		} 
		else
		{
			int l,r,v;
			cin >> l >> r >> v;
			modify(root,l,r,v);
		}
	}
		
	return 0;
}

可持久化数据结构

对于一个可能经过多次修改的数据结构,我们可能会需要找到他历史状态中的数据,
此时就可以用可持久化数据结构(强制在线)

可持久化数组

可以使用

pair<pair<int,int>,int> 变量名

来实现,三个值分别代表位置,时间,值

可持久化线段树

对于每次经过修改的线段树(单点修改),有且仅有一条链与原线段树不同,
我们考虑只把不同的地方建出来,剩下的直接Ctrl+c复制过去

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 7;

int read()
{
	int x = 0, w = 1;
	char ch = getchar();
	while(ch > '9' || ch < '0')
	{
		if(ch == '-')
		{
			w = -1;
			ch = getchar();
		}
	}
	while(ch <= '9' && ch >= '0')
	{
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * w;
}

long long Qmi(int a, int b, int p)
{
	if(b == 0)
	{
		return 1%p;
	}
	
	if(b == 1)
	{
		return a%p;
	}
	
	long long ans = Qmi(a, b/2, p);
	
	ans = ans*ans%p;
	
	if(b % 2)
	{
		ans = ans*a%p;
	}
	
	return ans % p;
}

bool isprime(long long x)
{
	if(x <= 1)
	{
		return false;
	}
	if(x == 2)
	{
		return true;
	}
	if(x % 2 == 0)
	{
		return false;
	}
	for(int i = 3;  i <= sqrt(x);  i += 2)
	{
		if(x % i == 0)
		{
			return false;
		}
	}
	return true;
}

int cnt;

int a[MAXN], root[MAXN];

struct node
{
	int l, r;
	
	int sum;
	
	node()
	{
		l = r = sum = 0;
	}
	
}z[MAXN*20];//应是MAXN*logn,这里logn取20

void update(int p)
{
	z[p].sum = z[z[p].l].sum + z[z[p].r].sum;
}

int build(int l, int r)
{
	cnt++;
	int p = cnt;
	
	if(l == r)
	{
		z[p].sum = a[l];
		return p;
	}
	int m = (l + r) >> 1;
	
	z[p].l = build(l, m);
	z[p].r = build(m + 1, r);
	
	update(p);
	
	return p;
}

int query(int l, int r, int rt, int nowl, int nowr)
{
	if(nowl <= l && r <= nowr)
	{
		return z[rt].sum;
	}
	
	int m = (l + r) >> 1;
	
	if(nowl <= m)
	{
		if(m < nowr)
		{
			return query(l, m, z[rt].l, nowl, nowr) + query(m+1, r, z[rt].r, nowl, nowr);	
		}
		else
		{
			return query(l, m, z[rt].l, nowl, nowr);
		}
	}
	else
	{
		return query(m+1, r, z[rt].r, nowl, nowr);
	}
}

int modify(int l, int r, int rt, int p, int v)
{
	cnt++;
	
	int q = cnt;
	
	z[q] = z[rt];
	
	if(l == r)
	{
		z[q].sum += v;
		return q;
	}
	
	int m = (l + r) >> 1;
	if(p <= m)
	{
		z[q].l = modify(l, m, z[q].l, p, v);
	}
	else
	{
		z[q].r = modify(m+1, r, z[q].r, p, v);
	}
	
	update(q);
	return q;
}

int n, m;

int main()
{
	cin>>n;
	
	for(int i = 1;  i <= n;  i++)
	{
		cin>>a[i];
	}
	
	cin>>m;
	
	root[0] = build(1, n);
	
	for(int i = 1;  i <= m;  i++)
	{
		int op;
		cin>>op;
		
		if(op == 1)
		{
			int p, v;
			cin>>p>>v;
			
			root[i] = modify(1, n, root[i-1], p, v);
		}
		else
		{
			int k, l, r;
			cin>>k>>l>>r;
			cout<<query(1, n, root[k], l, r)<<'\n';
			root[i] = root[i-1];
		}
	}
	
	return 0;
}

前缀值域可持久化线段树(主席树)

前缀和+可持久化线段树

点击查看代码
#include <iostream>

using std::cin;
using std::cout;

const int N = 2e5 + 10;

struct Node
{
	int l, r; // 左儿子,右儿子
	int sum;	// 区间和
	Node()
	{
		l = r = sum = 0;
	}
} z[N * 30];

int n;
int cnt; // 节点数
int a[N];
int root[N]; // 第 i 个前缀应的值域线段树的根

void update(int p)
{
	z[p].sum = z[z[p].l].sum + z[z[p].r].sum;
}
int build(int l, int r) // 返回这段区间对应的节点编号
{
	cnt++;
	int p = cnt;
	if (l == r)
	{
		z[p].sum = a[l];
		return p;
	}
	int m = (l + r) >> 1;
	z[p].l = build(l, m);
	z[p].r = build(m + 1, r);
	update(p);
	return p;
}
int query(int p1, int p2, int l, int r, int k)
// 当前对应的值域范围 l ~ r
// 要询问第 k 小的数
// 需要 p1, p2 两棵线段树来询问
{
	if (l == r)
		return l;
	int m = (l + r) >> 1;
	if (z[z[p2].l].sum - z[z[p1].l].sum >= k)
		return query(z[p1].l, z[p2].l, l, m, k);
	else
		return query(z[p1].r, z[p2].r, m + 1, r, k - (z[z[p2].l].sum - z[z[p1].l].sum));
}
int modify(int l, int r, int rt, int p, int v)
{
	cnt++;
	int q = cnt; // 新的节点 q 用于修改
	z[q] = z[rt];
	if (l == r)
	{
		z[q].sum += v;
		return q;
	}
	int m = (l + r) >> 1;
	if (p <= m)
		z[q].l = modify(l, m, z[q].l, p, v);
	else
		z[q].r = modify(m + 1, r, z[q].r, p, v);
	update(q);
	return q;
}

int main()
{
	cin >> n;
	int MAXV = 0;
	for (int i = 1; i <= n; ++i)
		cin >> a[i], MAXV = std::max(MAXV, a[i]);
	int m;
	cin >> m;
	root[0] = 0;
	for (int i = 1; i <= n; ++i)
		root[i] = modify(1, MAXV, root[i - 1], a[i], 1);
	// 主席树就建好了
	for (int i = 1; i <= m; ++i)
	{
		int l, r, k;
		cin >> l >> r >> k;
		cout << query(root[l - 1], root[r], 1, MAXV, k);
	}
	return 0;
}

小技巧

1.换行

使用"\n"换行比endl快

#include<bits/stdc++.h>

using namespace std;

int main()
{
	freopen("1.txt","w",stdout);//重定向到文件输出  
	
	for (int i=1;i<=10000000;i++)
		//cout << i << endl;//14.08s
		cout << i << "\n";//0.9579s
	
	return 0;
}

2.双指针-区间计算

例:P1886滑动窗口

3.取模

众所周知,取模是一种速度很慢的运算,所以我们要考虑
在不影响答案正确性的前提下,尽可能少的进行取模运算
code

点击查看代码
#include<bits/stdc++.h>

using namespace std;

const int mo = 1000000007;

inline void inc(int &a,int b)
{
	a+=b;if (a>=mo) a-=mo;
}
#define inc(a,b) {a+=b;if (a>=mo) a-=mo;}

int main()
{
	res.sum = (1ll * res.sum * mul + 1ll * add * res.size) % mo;
	
	//a = b+c*d;
	a = (b + 1ll * c * d) % mo;
	
	//a = b*c*d + e*f + g;
	a = (1ll * b * c % mo * d + 1ll * e * f + g) % mo;
	
	//a = a + b;
	a += b; if (a >= mo) a-=mo;
	
	//a = b*c-d*e
	a = ((1ll * b * c - 1ll * d * e) % mo + mo) % mo;
	
	//a = a - b
	a -= b; if (a<0) a += mo;
	
	return 0;
}
posted @ 2025-05-01 19:44  yysssqq  阅读(35)  评论(0)    收藏  举报