模板 - 主席树

255. 第K小数 - AcWing题库

又名可持久化线段树

不清楚主席树是不是一定是基于值域建的,这道模板题是基于值域的可持久化线段树

找第k小类似于二分思想,左半边如果数字个数<=k,就往左边走,反之往右边走,最后停留的位置就是第k小数的位置。

计算tr的空间时用log2算完向上取整后尽量再多加1,跟线段树开4倍空间的道理差不多,不然数据强的话可能会wa

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
#define fixed(s) fixed<<setprecision(12)<<s
//#define int long long
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 100010, M = 17;

int n, m;
int a[N];
vector<int> alls;
struct Node
{
	int l, r;//左孩子 右孩子 
	int cnt;//这个数值范围内点的个数 
}tr[N * 4 + N * M];
int root[N], idx;

int find(int x)
{
	return lower_bound(alls.begin(), alls.end(), x) - alls.begin();
}

int build(int l, int r)//范围 
{
	int p = ++ idx;
	if(l == r)return p;
	int mid = l + r >> 1;
	tr[p].l = build(l, mid), tr[p].r = build(mid + 1, r);
	return p;
}

int insert(int p, int l, int r, int x)//p是上一个根节点 
{
	int q = ++ idx;
	tr[q] = tr[p];
	
	if(l == r)
	{
		tr[q].cnt ++;
		return q;
	}
	
	int mid = l + r >> 1;
	if(x <= mid)tr[q].l = insert(tr[p].l, l, mid, x);
	else tr[q].r = insert(tr[p].r, mid + 1, r, x);
	tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
	
	return q;
}

int query(int p, int q, int l, int r, int k)
{
	if(l == r)return l;
	
	int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
	int mid = l + r >> 1;
	if(k <= cnt)return query(tr[p].l, tr[q].l, l, mid, k);
	else return query(tr[p].r, tr[q].r, mid + 1, r, k - cnt);//记得把左半的减掉 
}

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 1; i <= n; i ++)
	{
		cin >> a[i];
		alls.push_back(a[i]);
	}
	
	sort(alls.begin(), alls.end());
	alls.erase(unique(alls.begin(), alls.end()), alls.end());
	
	root[0] = build(0, alls.size() - 1);
	for(int i = 1; i <= n; i ++)
	{
		root[i] = insert(root[i - 1], 0, alls.size() - 1, find(a[i]));
	}
	
	while(m --)
	{
		int l, r, k;
		cin >> l >> r >> k;
		cout << alls[query(root[l - 1], root[r], 0, alls.size() - 1, k)] << endl;
	}
	
	return 0;
}

        主席树也是可以进行单点修改的,以下图为例,1号点是root[1],3号点是root[2],3号点继承了一号点的左儿子,但通过3号点对左儿子进行修改时,并不会修改2号点,而是复制出一个6号点,修改的实际上是这个6号点,所以主席树某一个版本的单点修改并不会扰乱以前版本。(修改3号点时重新赋值为了5号点,因此2号点复制出来的是6号点)。

贴一个求区间内数字种数的板子

就是插入每个数x时令当前位置+1、上一个x的位置-1,因为统计种类数只需要一个数字就行了。

因为主席树可以记录历史版本,所以可以实现该操作。

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
#define fixed(s) fixed<<setprecision(12)<<s
//#define int long long
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1000010;

int n;
int root[N], idx;
int pos[N];
struct Node
{
	int l, r;
	int sum;
}tr[N * 40];

int insert(int p, int l, int r, int x, int c)
{
	int q = ++ idx;
	tr[q] = tr[p];
	if(l == r)
	{
		tr[q].sum += c;
		return q;
	}
	int mid = l + r >> 1;
	if(x <= mid)tr[q].l = insert(tr[p].l, l, mid, x, c);
	else tr[q].r = insert(tr[p].r, mid + 1, r, x, c);
	tr[q].sum = tr[tr[q].l].sum + tr[tr[q].r].sum;
	return q;
}

int query(int q, int l, int r, int ql, int qr)
{
	if(ql <= l && r <= qr)return tr[q].sum;
	
	int mid = l + r >> 1;
	
	if(qr <= mid)return query(tr[q].l, l, mid, ql, qr);
	else if(mid + 1 <= ql)return query(tr[q].r, mid + 1, r, ql, qr);
	else
	{
		return query(tr[q].l, l, mid, ql, qr) + query(tr[q].r, mid + 1, r, ql, qr);
	}
}

int main()
{
	scanf("%d", &n);
	int x;
	for(int i = 1; i <= n; ++ i)
	{
		scanf("%d", &x);
		root[i] = insert(root[i - 1], 1, n, i, 1);
		if(pos[x])root[i] = insert(root[i], 1, n, pos[x], -1);
		pos[x] = i;//pos数组用于存储a[i]的上一个出现位置,如果a[i]很大要用map 
	}
	
	int m, l, r;
	scanf("%d", &m);
	for(int i = 1; i <= m; ++ i)
	{
		scanf("%d%d", &l, &r);
		printf("%d\n", query(root[r], 1, n, l, n));
	 } 
	
	return 0;
}

1e6数据范围内2s大概率能过,也可能被卡

顺便贴一个树状数组版本的(稳过)

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
#define fixed(s) fixed<<setprecision(12)<<s
//#define int long long
 
using namespace std;
 
typedef pair<int, int> PII;
typedef long long ll;

const int N = 1000010;

int n;
int tr[N];
int ans[N];
int pos[N], a[N];
vector<PII> right1[N];//右端点 

int lowbit(int x)
{
	return x & -x;
}

void add(int x, int c)
{
	for(int i = x; i <= n; i += lowbit(i))
	{
		tr[i] += c;
	}
} 

int sum(int x)
{
	int res = 0;
	for(int i = x ; i; i -= lowbit(i))
	{
		res += tr[i];
	}
	return res;
}

int main()
{
	IOS
	map<PII, int> mp;
	
	cin >> n;
	for(int i = 1; i <= n; i ++)
	{
		cin >> a[i];
	}
	
	vector<PII> res;
	int m;
	cin >> m;
	for(int i = 1; i <= m; i ++)
	{
		int l, r;
		cin >> l >> r;
		right1[r].push_back({l, i});
	}
	
	for(int i = 1; i <= n; i ++)
	{
		add(i, 1);
		if(pos[a[i]])add(pos[a[i]], -1);
		pos[a[i]] = i;
		
		for(auto x : right1[i])
		{
			ans[x.second] = sum(i) - sum(x.first - 1);
		}
	}
	
	for(int i = 1; i <= m; i ++)
	{
		cout << ans[i] << endl;
	}
	
	return 0;
}

P2617 Dynamic Rankings - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

带修改的可持久化线段树:

修改某个根节点后势必会对后续全部根节点有影响,将后续全部根节点修改一遍的复杂度很高。

可持久化线段树和前缀和数组很像:都是T(x)=T(x-1)+c

前缀和数组可以用树状数组来解决这个问题,同理可持久化线段树亦可以。

不同的地方在于:root[i]不再基于root[i-1]进行创建,而是基于root[i]本身进行创建。

每次修改和查询的时间复杂度由log变为了log方

空间复杂度也变为了nlog^{2}

#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
#define endl '\n'
#define fixed(s) fixed<<setprecision(12)<<s
//#define int long long

using namespace std;

typedef pair<int, int> PII;
typedef pair<double, double> PDD;
typedef long long ll;

const int N = 100010;

struct Node
{
	int l, r, cnt;
}tr[N * 17 * 17];
int root[N], idx;
int n, m;
int a[N], q2[N], q3[N], q4[N];
char q1[N];
vector<int> alls;

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;
}

int lowbit(int x)
{
	return x & -x;
}

int insert(int p, int l, int r, int x, int c)
{
	if(!p)p = ++ idx;
	
	if(l == r)
	{
		tr[p].cnt += c;
		return p;
	}
	
	int mid = l + r >> 1;
	if(x <= mid)tr[p].l = insert(tr[p].l, l, mid, x, c);
	else tr[p].r = insert(tr[p].r, mid + 1, r, x, c);
	tr[p].cnt = tr[tr[p].l].cnt + tr[tr[p].r].cnt;
	return p;
}

void change(int pos, int c)
{
	for(int i = pos; i <= n; i += lowbit(i))
	{
		root[i] = insert(root[i], 0, alls.size() - 1, find(a[pos]), c);
	}
}

int t1[110], t2[110], n1, n2;//存查第k小时的节点 

int kth(int l, int r, int k)
{
	if(l == r)return r;
	
	int cnt = 0;
	for(int i = 1; i <= n2; i ++)
	{
		int p = t2[i];
		cnt += tr[tr[p].l].cnt;
	}
	for(int i = 1; i <= n1; i ++)
	{
		int p = t1[i];
		cnt -= tr[tr[p].l].cnt;
	}
	
	int mid = l + r >> 1;
	if(k <= cnt)
	{
		for(int i = 1; i <= n1; i ++)t1[i] = tr[t1[i]].l;
		for(int i = 1; i <= n2; i ++)t2[i] = tr[t2[i]].l;
		return kth(l, mid, k);
	}
	else
	{
		for(int i = 1; i <= n1; i ++)t1[i] = tr[t1[i]].r;
		for(int i = 1; i <= n2; i ++)t2[i] = tr[t2[i]].r;
		return kth(mid + 1, r, k - cnt);
	}
}

int query(int p, int q, int l, int r, int k)
{
	n1 = n2 = 0;
	for(int i = p; i; i -= lowbit(i))t1[++ n1] = root[i];
	for(int i = q; i; i -= lowbit(i))t2[++ n2] = root[i];
	
	return kth(l, r, k);
}

int main()
{
	IOS
	cin >> n >> m;
	for(int i = 1; i <= n ; i ++)
	{
		cin >> a[i];
		alls.push_back(a[i]);
	}
	for(int i = 1; i <= m; i ++)
	{
		cin >> q1[i];
		if(q1[i] == 'Q')cin >> q2[i] >> q3[i] >> q4[i];
		else
		{
			cin >> q2[i] >> q3[i];
			alls.push_back(q3[i]);
		}
	}
	
	sort(alls.begin(), alls.end());
	alls.erase(unique(alls.begin(), alls.end()), alls.end());
	
	for(int i = 1; i <= n; i ++)change(i, 1);
	
	for(int i = 1; i <= m; i ++)
	{
		if(q1[i] == 'Q')
		{
			int pos = query(q2[i] - 1, q3[i], 0, alls.size() - 1, q4[i]);
			//cout << pos << "===" << endl;
			cout << alls[pos] << endl;
		}
		else
		{
			change(q2[i], -1);
			a[q2[i]] = q3[i];
			change(q2[i], 1);
		}
	}
	
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值