线段树&IndexTree&AC自动机

线段树定义:

1,一种支持范围整体修改和范围整体查询的数据结构

2,解决的问题范畴:

大范围信息可以只由左、右两侧信息加工出,

而不必遍历左右两个子范围的具体状况

线段树例如:

给定一个数组arr,用户希望你实现如下三个方法

1void add(int L, int R, int V) :  让数组arr[L…R]上每个数都加上V

2void update(int L, int R, int V) :  让数组arr[L…R]上每个数都变成V

3int sum(int L, int R) :让返回arr[L…R]这个范围整体的累加和

怎么让这三个方法,时间复杂度都是O(logN)

思路:

1.每个arr[] 下表是从0到N-1  先变为1到N,这样arr[]就可以变成一个树的格式

2.sum就可以维护一个区间的和,且X的左树就是X*2 右树就是X*2+1

3.假如来一个add请求,我们当前X位置代表区间是 2-4 而来的请求是1-9加Y 那么我们就可以2-4的和加上(2-4整体个数)*Y,而有一个懒加载的数+Y

假如这个请求代表区间2-4 而请求是3-6,那么如果2-4有懒加载需要先把这个值下发到2-4下面的子树,再看这个3-6的请求在子树如何搞

4假如一个update 我们需要告诉下面有个update请求且会变成啥,一旦这个下发了则会置空下面add的懒加载,所以下发的方法肯定是先update再add

5查询一个道理,在区间返回sum 不在就下发了再收集

public static void main(String[] args) {
		int[] origin = { 2, 1, 1, 2, 3, 4, 5 };
		SegmentTree seg = new SegmentTree(origin);
		int S = 1; // 整个区间的开始位置,规定从1开始,不从0开始 -> 固定
		int N = origin.length; // 整个区间的结束位置,规定能到N,不是N-1 -> 固定
		int root = 1; // 整棵树的头节点位置,规定是1,不是0 -> 固定
		int L = 2; // 操作区间的开始位置 -> 可变
		int R = 5; // 操作区间的结束位置 -> 可变
		int C = 4; // 要加的数字或者要更新的数字 -> 可变
		// 区间生成,必须在[S,N]整个范围上build
		seg.build(S, N, root);
		// 区间修改,可以改变L、R和C的值,其他值不可改变
		seg.add(L, R, C, S, N, root);
		// 区间更新,可以改变L、R和C的值,其他值不可改变
		seg.update(L, R, C, S, N, root);
		// 区间查询,可以改变L和R的值,其他值不可改变
		long sum = seg.query(L, R, S, N, root);
		System.out.println(sum);
}

public static class SegmentTree {
		// arr[]为原序列的信息从0开始,但在arr里是从1开始的
		// sum[]模拟线段树维护区间和
		// lazy[]为累加和懒惰标记
		// change[]为更新的值
		// update[]为更新慵懒标记
		private int MAXN;
		private int[] arr;
		private int[] sum;
		private int[] lazy;
		private int[] change;
		private boolean[] update;

		public SegmentTree(int[] origin) {
			MAXN = origin.length + 1;
			arr = new int[MAXN]; // arr[0] 不用 从1开始使用
			for (int i = 1; i < MAXN; i++) {
				arr[i] = origin[i - 1];
			}
			sum = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围的累加和信息
			lazy = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務
			change = new int[MAXN << 2]; // 用来支持脑补概念中,某一个范围有没有更新操作的任务
			update = new boolean[MAXN << 2]; // 用来支持脑补概念中,某一个范围更新任务,更新成了什么
		}
		//给一个父节点,求2个子树的和
		private void pushUp(int rt) {
			sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
		}

		// 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围
		// 分发策略是什么
		// ln表示左子树元素结点个数,rn表示右子树结点个数
		private void pushDown(int rt, int ln, int rn) {
			if (update[rt]) {
				//左右都要有update了 把update下发了
				// 左右都变为change[rt] lazy还原为0 因为有update所以不再存在add的lazy
				//把左右子树的sum改了
				update[rt << 1] = true;
				update[rt << 1 | 1] = true;
				change[rt << 1] = change[rt];
				change[rt << 1 | 1] = change[rt];
				lazy[rt << 1] = 0;
				lazy[rt << 1 | 1] = 0;
				sum[rt << 1] = change[rt] * ln;
				sum[rt << 1 | 1] = change[rt] * rn;
				update[rt] = false;
			}
			//针对懒加载下发
			if (lazy[rt] != 0) {
				//下发左边的树
				lazy[rt << 1] += lazy[rt];
				sum[rt << 1] += lazy[rt] * ln;
				//下发右边的树
				lazy[rt << 1 | 1] += lazy[rt];
				sum[rt << 1 | 1] += lazy[rt] * rn;
				//当前树再有懒加载
				lazy[rt] = 0;
			}
		}

		// 在初始化阶段,先把sum数组,填好
		// 在arr[l~r]范围上,去build,1~N,
		// rt : 这个范围在sum中的下标
		public void build(int l, int r, int rt) {
			//如果左等于右说明 就一个了 那么sum就是他自己
			if (l == r) {
				sum[rt] = arr[l];
				return;
			}
			//二分左右区间 比如当前节点代表的是1-10整体  分开左边就是1-5 右边就是6-10
			//rt此时假如第一次进来为1
			int mid = (l + r) >> 1;
			//左边 1-5    rt变为1的左子树2
			build(l, mid, rt << 1);
			//右边 6-10  rt变为1的右子树3
			build(mid + 1, r, rt << 1 | 1);
			//合并1的左边和右边值 得到sum[rt]
			pushUp(rt);
		}

		
		// L~R  所有的值变成C
		// l~r  rt
		public void update(int L, int R, int C, int l, int r, int rt) {
			if (L <= l && r <= R) {
				update[rt] = true;
				change[rt] = C;
				sum[rt] = C * (r - l + 1);
				lazy[rt] = 0;
				return;
			}
			// 当前任务躲不掉,无法懒更新,要往下发
			int mid = (l + r) >> 1;
			pushDown(rt, mid - l + 1, r - mid);
			if (L <= mid) {
				update(L, R, C, l, mid, rt << 1);
			}
			if (R > mid) {
				update(L, R, C, mid + 1, r, rt << 1 | 1);
			}
			pushUp(rt);
		}

		// L~R, C 任务!
		// rt,l~r
		public void add(int L, int R, int C, int l, int r, int rt) {
			// 任务如果把此时的范围全包了!
			if (L <= l && r <= R) {
				//1这个位置 sum 就是 C*所有的树的和
				sum[rt] += C * (r - l + 1);
				//不往下传递 懒加载一个C
				lazy[rt] += C;
				return;
			}
			// 任务没有把你全包! 懒加载下发
			// l  r  mid = (l+r)/2
			int mid = (l + r) >> 1;
			pushDown(rt, mid - l + 1, r - mid);
			// L~R
			if (L <= mid) {
				//左边整体做add
				add(L, R, C, l, mid, rt << 1);
			}
			if (R > mid) {
				//右边整体做add
				add(L, R, C, mid + 1, r, rt << 1 | 1);
			}
			pushUp(rt);
		}

		// 1~6 累加和是多少? 1~8 rt
		public long query(int L, int R, int l, int r, int rt) {
			if (L <= l && r <= R) {
				return sum[rt];
			}
			int mid = (l + r) >> 1;
			pushDown(rt, mid - l + 1, r - mid);
			long ans = 0;
			if (L <= mid) {
				ans += query(L, R, l, mid, rt << 1);
			}
			if (R > mid) {
				ans += query(L, R, mid + 1, r, rt << 1 | 1);
			}
			return ans;
		}

	}

线段树题目:

Loading...Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.https://leetcode.com/problems/falling-squares/

想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线

下面是这个游戏的简化版:

1)只会下落正方形积木

2[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落

3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的

4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。

给定一个N*2的二维数组matrix,可以代表N个积木依次掉落,

返回每一次掉落之后的最大高度

public static class SegmentTree {
		private int[] max;
		private int[] change;
		private boolean[] update;

		public SegmentTree(int size) {
			int N = size + 1;
			max = new int[N << 2];

			change = new int[N << 2];
			update = new boolean[N << 2];
		}

		private void pushUp(int rt) {
			max[rt] = Math.max(max[rt << 1], max[rt << 1 | 1]);
		}

		// ln表示左子树元素结点个数,rn表示右子树结点个数
		private void pushDown(int rt, int ln, int rn) {
			if (update[rt]) {
				update[rt << 1] = true;
				update[rt << 1 | 1] = true;
				change[rt << 1] = change[rt];
				change[rt << 1 | 1] = change[rt];
				max[rt << 1] = change[rt];
				max[rt << 1 | 1] = change[rt];
				update[rt] = false;
			}
		}

		public void update(int L, int R, int C, int l, int r, int rt) {
			if (L <= l && r <= R) {
				update[rt] = true;
				change[rt] = C;
				max[rt] = C;
				return;
			}
			int mid = (l + r) >> 1;
			pushDown(rt, mid - l + 1, r - mid);
			if (L <= mid) {
				update(L, R, C, l, mid, rt << 1);
			}
			if (R > mid) {
				update(L, R, C, mid + 1, r, rt << 1 | 1);
			}
			pushUp(rt);
		}

		public int query(int L, int R, int l, int r, int rt) {
			if (L <= l && r <= R) {
				return max[rt];
			}
			int mid = (l + r) >> 1;
			pushDown(rt, mid - l + 1, r - mid);
			int left = 0;
			int right = 0;
			if (L <= mid) {
				left = query(L, R, l, mid, rt << 1);
			}
			if (R > mid) {
				right = query(L, R, mid + 1, r, rt << 1 | 1);
			}
			return Math.max(left, right);
		}

	}
	//将二维数组下标化  比如[[1,2],[3,3]]  变为下标 key左右边界,value为下标从1到N  {1,1}{2,2}{5,3}
	public HashMap<Integer, Integer> index(int[][] positions) {
		TreeSet<Integer> pos = new TreeSet<>();
		for (int[] arr : positions) {
			pos.add(arr[0]);
			pos.add(arr[0] + arr[1] - 1);
		}
		HashMap<Integer, Integer> map = new HashMap<>();
		int count = 0;
		for (Integer index : pos) {
			map.put(index, ++count);
		}
		return map;
	}

	public List<Integer> fallingSquares(int[][] positions) {
		HashMap<Integer, Integer> map = index(positions);
		int N = map.size();
		SegmentTree segmentTree = new SegmentTree(N);
		int max = 0;
		List<Integer> res = new ArrayList<>();
		// 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么
		for (int[] arr : positions) {
			//拿到2个下标
			int L = map.get(arr[0]);
			int R = map.get(arr[0] + arr[1] - 1);
			//查找1-N中L-R的height + 当前的arr[1]就是L-R实际height
			int height = segmentTree.query(L, R, 1, N, 1) + arr[1];
			max = Math.max(max, height);
			res.add(max);
			//更新L-R的height
			segmentTree.update(L, R, height, 1, N, 1);
		}
		return res;
	}

IndexTree:

特点:

1)支持区间查询

2)没有线段树那么强,但是非常容易改成一维、二维、三维的结构

3)只支持单点更新

给一个数组下标为 1,2,3,4,5,6,7,8

前缀和 则为[【1】,【1,2】,【1,2,3】,【1,2,3,4】,【1,2,3,4,5】,【1,2,3,4,5,6】,【1,2,3,4,5,6,7】,【1,2,3,4,5,6,7,8】]

那么index前缀和

【1】【1,2】【3】【1,2,3,4】【5】【5,6】【7】【1,2,3,4,5,6,7,8】

  1            2        3              4                   5         6            7         8                      下标

 意思就是 到一个下标先自己成为一个整体,看之前有没有和自己一样的数量整体,如果有那么就合并一起管理

那么如果我改了下标3 加10 那么需要更改的下标是 4,8 如何得到呢,有个好玩的方法:

3二进制  ...011  那么他加上取反+1就是下一个位置(index & -index)或者是(index & ~index+1)

也就是...001+...011 结果就是..100=4

4继续+....100 就是 ...1000=8

那么每个下标管理的是哪些和呢?

就是自己的二进制到 自己二进制减去(index & -index)  比如 011-001=2 所以3只管理自己

这样就得到了index的基础代码

// 下标从1开始!
	public static class IndexTree {

		private int[] tree;
		private int N;

		// 0位置弃而不用!
		public IndexTree(int size) {
			N = size;
			tree = new int[N + 1];
		}

		// 1~index 累加和是多少?
		public int sum(int index) {
			int ret = 0;
			while (index > 0) {
				ret += tree[index];
				index -= index & -index;
			}
			return ret;
		}

		// index & -index : 提取出index最右侧的1出来
		// index :           0011001000
		// index & -index :  0000001000(index & (~index+1))
		public void add(int index, int d) {
			while (index <= N) {
				tree[index] += d;
				index += index & -index;
			}
		}
	}

如果indexTree改为二维的就是 二维的2个数 都管理成上面的方式 [X,Y]管理的就是( X-X&-X到X )*( Y-Y&-Y到Y)

AC自动机

解决在一个大字符串中,找到多个候选字符串的问题

1)把所有匹配串生成一棵前缀树

2)前缀树节点增加fail指针

3fail指针的含义:如果必须以当前字符结尾,当前形成的路径是str,剩下哪一个字符串的前缀和str的后缀,拥有最大的匹配长度。fail指针就指向那个字符串的最后一个字符所对应的节点。看虚线

 

	// 前缀树的节点
	public static class Node {
		// 如果一个node,end为空,不是结尾
		// 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串
		public String end;
		// 只有在上面的end变量不为空的时候,endUse才有意义
		// 表示,这个字符串之前有没有加入过答案
		public boolean endUse;
		public Node fail;
		public Node[] nexts;

		public Node() {
			endUse = false;
			end = null;
			fail = null;
			nexts = new Node[26];
		}
	}

	public static class ACAutomation {
		private Node root;

		public ACAutomation() {
			root = new Node();
		}

		public void insert(String s) {
			char[] str = s.toCharArray();
			Node cur = root;
			int index = 0;
			for (int i = 0; i < str.length; i++) {
				index = str[i] - 'a';
				if (cur.nexts[index] == null) {
					cur.nexts[index] = new Node();
				}
				cur = cur.nexts[index];
			}
			cur.end = s;
		}

		public void build() {
			Queue<Node> queue = new LinkedList<>();
			queue.add(root);
			Node cur = null;
			Node cfail = null;
			while (!queue.isEmpty()) {
				// 某个父亲,cur
				cur = queue.poll();
				for (int i = 0; i < 26; i++) { // 所有的路
					// cur -> 父亲  i号儿子,必须把i号儿子的fail指针设置好!
					if (cur.nexts[i] != null) { // 如果真的有i号儿子
						cur.nexts[i].fail = root;
						cfail = cur.fail;
						while (cfail != null) {
							if (cfail.nexts[i] != null) {
								cur.nexts[i].fail = cfail.nexts[i];
								break;
							}
							cfail = cfail.fail;
						}
						queue.add(cur.nexts[i]);
					}
				}
			}
		}

		// 大文章:content
		public List<String> containWords(String content) {
			char[] str = content.toCharArray();
			Node cur = root;
			Node follow = null;
			int index = 0;
			List<String> ans = new ArrayList<>();
			for (int i = 0; i < str.length; i++) {
				index = str[i] - 'a'; // 路
				// 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径
				while (cur.nexts[index] == null && cur != root) {
					cur = cur.fail;
				}
				// 1) 现在来到的路径,是可以继续匹配的
				// 2) 现在来到的节点,就是前缀树的根节点
				cur = cur.nexts[index] != null ? cur.nexts[index] : root;
				follow = cur;
				while (follow != root) {
					if (follow.endUse) {
						break;
					}
					// 不同的需求,在这一段之间修改
					if (follow.end != null) {
						ans.add(follow.end);
						follow.endUse = true;
					}
					// 不同的需求,在这一段之间修改
					follow = follow.fail;
				}
			}
			return ans;
		}

	}

	public static void main(String[] args) {
		ACAutomation ac = new ACAutomation();
		ac.insert("dhe");
		ac.insert("he");
		ac.insert("abcdheks");
		// 设置fail指针
		ac.build();

		List<String> contains = ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv");
		for (String word : contains) {
			System.out.println(word);
		}
	}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

普朗克的朗姆酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值