首页 > 编程语言 >[算法] 一些分治

[算法] 一些分治

时间:2024-08-06 19:49:22浏览次数:11  
标签:int 分治 mid long 算法 一些 500005 id

普通分治

其实没啥,每次只计算跨越分治中心的区间的贡献,剩下的递归到左右两边进行分治;

时间复杂度:分治树高度为 $ \Theta (\log n) $,乘上其他操作的复杂度即可;

例题一:现在有一个 $ n $ 阶排列 $ a $,计算:

\[ \sum^{n}_{i = 1} \sum^{n}_{j = i} \min(a_i, a_{i + 1} ,..., a_j) \]

其中 $ n \leq 200000 $

题意简述:找一个给定的序列的所有子区间的最小值的和;

可以线性做,对于每一个 $ a_i $,记录其向左和向右第一个小于它的值,计算一下即可;

这里讲一下分治的做法;

其实对于这种求所有区间中符合条件的区间的题目,一般都可以分治做;

考虑跨过分治中心的区间,设分治中心为 $ mid $, 我们可以从分治中心向左维护出对于任意一个左端点 $ l $,区间 $ [l, mid] $ 的最小值并存放在一个数组 $ b $ 中;

处理完上述步骤后,我们开始从分治中心向右遍历右端点,每遍历到一个右端点 $ r $,我们发现它的所有跨过分治中心的区间的最小值有两种情况:

  1. 是区间 $ [mid, r] $ 中的值;

  2. 是 $ mid $ 左边的值;

对于第一种情况,我们每次遍历时维护一个最小值 $ mi $ 即可;

对于第二种情况,暴力做法肯定不行,那怎么办呢?

挖掘一下性质,我们发现 $ b $ 数组中的值是非严格单调递减的(因为最小值只能不变或者更小,不会变大);

所以,我们可以每次遍历右端点时用二分查找找出 $ b $ 数组中第一个大于 $ mi $ 的位置,然后小于等于它的最小值不变,大于它的最小值变为 $ mi $;

为了避免手写二分(懒,不想写),我们可以将 $ b $ 数组 $ reverse $ 一下,然后用 $ upper \ bound $ 即可;

当然,还要维护一个前缀和(注意是 $ reverse $ 后的);

时间复杂度:分治 + 遍历 $ \Theta(n \log n) $,二分查找 $ \Theta(\log n) $,总的 $ \Theta(n \log^2 n) $ (时间复杂度确实劣了一些);

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
long long n;
long long a[500005];
long long b[500005], sum[500005];
long long c[500005];
long long ans;
void solve(long long l, long long r) {
	if (l >= r) return;
	if (r - l + 1 == 2) {
		ans += min(a[l], a[r]);
		return;
	}
	long long mid = (l + r) >> 1;
	long long o = 0;
	for (int i = 0; i <= mid - l; i++) b[i] = 0x3f3f3f3f;
	for (long long i = mid - 1; i >= l; i--) {
		o++;
		b[o] = min(b[o - 1], a[i]);
	}
	long long cnt = 0;
	for (long long i = o; i >= 1; i--) {
		c[++cnt] = b[i];
	}
	for (long long i = 0; i <= cnt; i++) {
		sum[i] = 0;
	}
	for (long long i = 1; i <= cnt; i++) {
		sum[i] = sum[i - 1] + c[i];
	}
	long long mi = a[mid];
	for (long long j = mid + 1; j <= r; j++) {
		mi = min(mi, a[j]);
		long long pos = upper_bound(c + 1, c + 1 + cnt, mi) - c;
		if (pos <= cnt) ans += (cnt - pos + 1) * mi;
		ans += sum[pos - 1];
	}
	solve(l, mid);
	solve(mid, r);
}
int main() {
	cin >> n;
	for (long long i = 1; i <= n; i++) {
		cin >> a[i];
	}
	solve(1, n);
	for (long long i = 1; i <= n; i++) ans += a[i];
	cout << ans;
	return 0;
}

其实很多分治的套路是维护前缀和 + 发现性质,做的时候可以注意一下;

例题二: Luogu P4062 [Code+#1] Yazid 的新生舞会

这题貌似题解中的主流做法是用数据结构维护高阶前缀和,这里讲一下分治做法;

还是求所有区间中符合条件的区间,可以考虑分治;

找区间中的绝对众数,我们可以借鉴一下摩尔投票法,设现在我们考虑的众数为 $ x $,将不是 $ x $ 的数看为 $ -1 $,是 $ x $ 的数看为 $ 1 $,最后判断一下整个区间的和与 $ 0 $ 的关系即可;

首先,对于一个区间 $ [l, r] $,其绝对众数是 $ [l, k] $ 的绝对众数或 $ [k + 1, r] $ 的绝对众数,其中 $ l \leq k \leq r $;

所以可以令 $ k = mid $,然后分治求解;

分治时,还是先从分治中心向左找符合条件的绝对众数以及区间和所出现的次数,向右遍历时统计一下区间和是否 $ > 0 $ 即可;

由于我们要遍历绝对众数,而绝对众数最多变化 $ \Theta (\log n) $ 次(相当于每次砍一半才能更新一次绝对众数),所以总的时间复杂度为 $ \Theta (n \log^2 n) $;

写的比较粗略,可以参考一下原题解区的题解;

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
int n, ddd;
int a[1000005];
long long ans;
int pos[1000005], vis[1000005], num[1000005], cnt[1000005];
void solve(int l, int r) {
	if (l == r) {
		ans++;
		return;
	}
	int mid = (l + r) >> 1;
	num[0] = 0;
	for (int i = mid; i >= l; i--) {
		if (++cnt[a[i]] > (mid - i + 1) / 2) {
			if (!pos[a[i]]) {
				pos[a[i]] = ++num[0];
				num[pos[a[i]]] = a[i];
			}
		}
	}
	for (int i = mid + 1; i <= r; i++) {
		if (++cnt[a[i]] > (i - mid) / 2) {
			if (!pos[a[i]]) {
				pos[a[i]] = ++num[0];
				num[pos[a[i]]] = a[i];
			}
		}
	}
	for (int i = l; i <= r; i++) {
		pos[a[i]] = 0;
		cnt[a[i]] = 0;
	}
	for (int i = 1; i <= num[0]; i++) {
		int sum = r - l + 1;
		int ma = sum;
		int mi = sum;
		cnt[sum] = 1;
		for (int j = l; j < mid; j++) {
			if (a[j] == num[i]) {
				sum++;
			} else {
				sum--;
			}
			ma = max(ma, sum);
			mi = min(mi, sum);
			cnt[sum]++;
		}
		if (a[mid] == num[i]) {
			sum++;
		} else {
			sum--;
		}
		for (int j = mi; j <= ma; j++) {
			cnt[j] += cnt[j - 1];
		}
		for (int j = mid + 1; j <= r; j++) {
			if (a[j] == num[i]) {
				sum++;
			} else {
				sum--;
			}
			ans += cnt[min(sum - 1, ma)];
		}
		for (int j = mi; j <= ma; j++) {
			cnt[j] = 0;
		}
	}
	solve(l, mid);
	solve(mid + 1, r);
}
int main() {
	cin >> n >> ddd;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	solve(1, n);
	cout << ans;
	return 0;
}

猫树分治

实话说,我就做过一个关于这个的题,感觉和普通分治没有什么区别;

直接粘我以前写的题解了(出处):

Luogu P6240 好吃的题目

暴力一:每次跑一边 $ DP $;

暴力二:使用背包的合并操作,时间复杂度 $ \Theta(n^2) $ ?;

正解:猫树分治;

这玩意听着这么像数据结构,其实就是一个套路;

好像它的发明者受到了线段树分治的启发?

和普通的分治没什么区别,难的是想到分治(所以才给它起了个名字嘛);

每次只计算跨过分治中心的区间,首先预处理出从分治中心向左和向右的每个点到终点这段区间的所有 $ 200 $ 个最优值,然后进行合并,注意要将不跨过分治中心的区间筛选出来,分别放在左右两边,然后继续递归;

所以我们需要四个指针,两个记录现在处理的序列上的左右端点,另外两个记录现在处理的问题的区间(这里的 “区间” 并不绝对,只要是没处理的,都能出现在这一段区间),然后正常递归即可;

时间复杂度:$ T(200n \log n) $;

点击查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n, m;
int h[500005], w[500005];
struct sss{
	int l, r, t;
}b[500005];
int ans[500005], p[500005], s[500005], ncnt;
int f[50005][205];
void solve(int l, int r, int L, int R) {
	if (L > R) return;
	int mid = (l + r) >> 1;
	int Mid = L - 1;
	for (int i = 0; i <= 200; i++) f[mid][i] = 0;
	for (int i = mid + 1; i <= r; i++) {
		for (int j = 0; j < h[i]; j++) f[i][j] = f[i - 1][j];
		for (int j = h[i]; j <= 200; j++) {
			f[i][j] = max(f[i - 1][j], f[i - 1][j - h[i]] + w[i]);
		}
	}
	for (int i = h[mid]; i <= 200; i++) f[mid][i] = w[mid];
	for (int i = mid - 1; i >= l; i--) {
		for (int j = 0; j < h[i]; j++) f[i][j] = f[i + 1][j];
		for (int j = h[i]; j <= 200; j++) {
			f[i][j] = max(f[i + 1][j], f[i + 1][j - h[i]] + w[i]);
		}
	}
	ncnt = 0;
	int u = 0;
	for (int i = L; i <= R; i++) {
		u = p[i];
		if (b[u].r <= mid) p[++Mid] = u;
		else if (mid < b[u].l) s[++ncnt] = u;
		else {
			int ret = 0;
			for (int i = 0; i <= b[u].t; i++) {
				ret = max(ret, f[b[u].l][i] + f[b[u].r][b[u].t - i]);
			}
			ans[u] = ret;
		}
	}
	for (int i = 1; i <= ncnt; i++) {
		p[Mid + i] = s[i];
	}
	R = ncnt + Mid;
	solve(l, mid, L, Mid);
	solve(mid + 1, r, Mid + 1, R);
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for (register int i = 1; i <= n; i++) {
		cin >> h[i];
	}
	for (register int i = 1; i <= n; i++) {
		cin >> w[i];
	}
	for (register int i = 1; i <= m; i++) {
		cin >> b[i].l >> b[i].r >> b[i].t;
		if (b[i].l == b[i].r) {
			if (b[i].t >= h[b[i].l]) ans[i] = w[b[i].l];
		} else {
			p[++ncnt] = i;
		}
	}
	solve(1, n, 1, ncnt);
	for (int i = 1; i <= m; i++) {
		cout << ans[i] << endl;
	}
	return 0;
}

线段树分治

在区间上的操作,线段树好像都能干,并且它长得就很能分治,所以用它也并不奇怪;

Luogu P5787 二分图 /【模板】线段树分治

看见这种在时间线上的题,一般好像可以用线段树分治来做;

以前好像还有一道,但是忘了;

首先判断二分图,我们使用可撤销的扩展域并查集,具体可以看看原题解区;

具体的,我们对于这一条时间线开一个线段树,每个节点开一个动态数组存这个点所管辖的时间段内所加边的下标,最后从根开始 $ dfs $ 一下即可;

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <stack>
using namespace std;
int n, m, k;
stack<pair<int, pair<int, int> > > s;
int fa[500005];
int u[500005], t[500005];
int siz[500005];
int find(int x) {
	return (x == fa[x]) ? x : find(fa[x]);
}
namespace seg{
	inline int ls(int x) {
		return x << 1;
	}
	inline int rs(int x) {
		return x << 1 | 1;
	}
	struct sss{
		int l, r;
		vector<int> v;
	}tr[500005];
	void bt(int id, int l, int r) {
		tr[id].l = l;
		tr[id].r = r;
		if (l == r) {
			return;
		}
		int mid = (l + r) >> 1;
		bt(ls(id), l, mid);
		bt(rs(id), mid + 1, r);
	}
	void add(int id, int l, int r, int d) {
		if (tr[id].l >= l && tr[id].r <= r) {
			tr[id].v.push_back(d);
			return;
		}
		int mid = (tr[id].l + tr[id].r) >> 1;
		if (l <= mid) add(ls(id), l, r, d);
		if (r > mid) add(rs(id), l, r, d);
	}
}
void merge(int x, int y) {
	if (x == y) return;
	if (siz[x] > siz[y]) swap(x, y);
	s.push({y, {siz[x], x}});
	fa[x] = y;
	siz[y] += siz[x];
}
void dfs(int id) {
	bool vis = true;
	int o = s.size();
	for (int i = 0; i < seg::tr[id].v.size(); i++) {
		int x = seg::tr[id].v[i];
		int uu = find(u[x]);
		int tt = find(t[x]);
		if (uu == tt) {
			for (int j = seg::tr[id].l; j <= seg::tr[id].r; j++) cout << "No" << endl;
			vis = false;
			break;
		}
		merge(find(u[x] + n), tt);
		merge(find(t[x] + n), uu);
	}
	if (vis) {
		if (seg::tr[id].l == seg::tr[id].r) {
			cout << "Yes" << endl;
		} else {
			dfs(seg::ls(id));
			dfs(seg::rs(id));
		}
	}
	while(s.size() > o) {
		siz[s.top().first] -= s.top().second.first;
		fa[s.top().second.second] = s.top().second.second;
		s.pop();
	}
}
int main() {
	cin >> n >> m >> k;
	seg::bt(1, 1, k);
	int l, r;
	for (int i = 1; i <= m; i++) {
		cin >> u[i] >> t[i] >> l >> r;
		if (l != r) {
			seg::add(1, l + 1, r, i);
		}
	}
	for (int i = 1; i <= 2 * n; i++) {
		fa[i] = i;
		siz[i] = 1;
	}
	dfs(1);
	return 0;
}

可能以后看见了新的套路等等还会再来补充;

标签:int,分治,mid,long,算法,一些,500005,id
From: https://www.cnblogs.com/PeppaEvenPig/p/18345763

相关文章

  • 排序算法 堆排序 HeapSort -- C语言实现
    堆排序堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:大顶堆:每个节点的值都大于或等于其子......
  • 使用python解决一些计算 我们代码不比计算机差!
    使用python解决一些计算我们代码不比计算机差!一.简单基础计算1.基本的计算符号加+减-乘*****除/取余%乘方******整除//加减乘除不必多说说说比较陌生的取余乘方与整除取余%:10%3-->110-3-3-3=1最后剩下的数就是余数整除//:10//3-->310除3=3.333333去掉......
  • 排序算法 选择排序 SelectSort -- C语言实现
    选择排序描述选择排序是一种简单直观的排序算法,无论什么数据进去都是O(n²)的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了。算法步骤首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。再从剩余未排序元素中继续寻......
  • java解一些算法题
    题目描述某部门计划通过结队编程来进行项目开发,已知该部门有N名员工,每个员工有独一无二的职级,每三个员工形成一个小组进行结队编程。结队分组规则如下:从部门中选出序号分别为i、j、k的3名员工,他们的职级分别为level[i],level[j],level[k]结队小组需满足level[i]<le......
  • 代码随想录算法训练营第59天 | 最小生成树
    53.寻宝https://kamacoder.com/problempage.php?pid=1053prim算法精讲https://www.programmercarl.com/kamacoder/0053.寻宝-prim.htmlkruskal算法精讲https://www.programmercarl.com/kamacoder/0053.寻宝-Kruskal.html题目描述在世界的某个区域,有一些分散的神秘岛屿,每......
  • 排序算法 冒泡排序 BubbleSort -- C语言实现
    冒泡排序描述冒泡排序(BubbleSort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢......
  • Java-自然启发的算法教程-全-
    Java自然启发的算法教程(全)原文:Nature-InspiredOptimizationAlgorithmswithJava协议:CCBY-NC-SA4.0一、最优化导论:问题与技术真正的优化是现代研究对决策过程的革命性贡献。—乔治·丹齐格,美国科学家本章介绍了优化技术,重点是那些元启发/自然启发的技术。您将学习......
  • 算法随笔——高级树形DP
    换根DP学习博客https://www.luogu.com.cn/article/wdk0q56f树上背包https://www.luogu.com.cn/problem/P1273简单题意叶子节点有权值,每条边有花费,问最多连接多少个叶子节点到根,使得权值总和大于花费总和。做法设\(dp_{i,x,j}\)为在第x个节点,使用前i个子节点的子树,选j......
  • 局部非均值算法
    NLM是一种基于图像块相似性的图像去噪方法,由AntoniBuades等人于2005年提出。与传统的基于像素的局部滤波方法不同,NLM利用了图像的自相似性原理,即图像中的大部分结构会在不同的位置重复出现。这种方法在保持边缘清晰度和细节的同时,有效地减少了噪声的影响。  NLM降噪的基本步......
  • 20240806:点分治,虚树选做
    POJ-1741Tree题意:给定一棵树,求多少无序对\((u,v)\)满足\(\text{dist}(u,v)\lek\)。对于树上的路径进行分类:经过根;不经过根。第二类路径点可以通过递归子树求出。对于经过根的路径,可以一遍dfs求出每个点到根的距离\(\text{dis}(u)\)。问题转化为求\(\text{dis}(u)......