算法拾遗三十五indexTree和AC自动机

算法拾遗三十五indexTree和AC自动机

      • indexTree(树状数组)
        • indexTree规则
      • IndexTree二维
      • AC自动机

indexTree(树状数组)

给定数组下标统一从1开始
算法拾遗三十五indexTree和AC自动机_第1张图片
如果要求L。。R范围上任意区间的和,我们通常的解法是定义一个help(前缀和数组)数组,然后记录它的累加和
算法拾遗三十五indexTree和AC自动机_第2张图片
如果把arr中的某个数字改一下,则需要重新维护help数组。
如果我想同时支持arr中某个数字修改,也要快速查询出L。。R范围上的区间和,那么如上结构是不适用的。
indexTree不像线段树一样可以实现范围更新,indexTree适合单点更新查区间范围和的情况且时间复杂度相对于线段树更优

indexTree规则

算法拾遗三十五indexTree和AC自动机_第3张图片

有如下数组,下标从1开始:
算法拾遗三十五indexTree和AC自动机_第4张图片
我想生成另一个help数组:
当前来到1位置,我前面没有和我长度为1的凑一对,所以help的1位置填入3,当来到2位置的时候,前面有和我长度为1的凑一对,2位置填入4。当来到3位置的时候,前面没有单独和我长度为1的凑一对,3位置填入-2,4位置前面有和我长度为1的凑一对,然后再往前看前面有没有长度为2的和我凑一对,发现有的所以4位置填5,然后依次类推下去。
算法拾遗三十五indexTree和AC自动机_第5张图片
上图中规律:
如果index=8,它应该管1-8范围的累加和,
8的二进制位01000,它管的范围为将二进制中最后一个1拆散之后再加1的第一个数到自己
00001-01000(就是1-8)
再来一个例子当index=12的时候,应该管9-12范围的和 12的二进制为01100,则管的范围可以化简为01001-01100【9-12】

已知了如上规律,有help数组之后,那么如何利用help数组求前缀和,如果要求33位置的前缀和,
则就只需要求0100001+0100000【抹掉最后一个1的位置】,一直抹完所有最后一个1的位置。
算法拾遗三十五indexTree和AC自动机_第6张图片
比如:
help【0110100】,原始arr中想要累加到它
help[0110100] = arr[0110001-0110100]
第二步
help[0110000]=arr[0100001-0110000]
分段求得的解的和就是0110100区间范围的累加和。

时间复杂度是logN水平,因为动的是位数

public class IndexTree {

	// 下标从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) {
			//如果要把index位置增加一个d
			while (index <= N) {
				tree[index] += d;
				//index变化是当前index加上最右侧的1组成的数
				index += index & -index;
			}
		}
		//求影响的时候则由最右侧的1怼上去,累加就由最右侧的一减下来
	}

	public static class Right {
		private int[] nums;
		private int N;

		public Right(int size) {
			N = size + 1;
			nums = new int[N + 1];
		}

		public int sum(int index) {
			int ret = 0;
			for (int i = 1; i <= index; i++) {
				ret += nums[i];
			}
			return ret;
		}

		public void add(int index, int d) {
			nums[index] += d;
		}

	}

	public static void main(String[] args) {
		int N = 100;
		int V = 100;
		int testTime = 2000000;
		IndexTree tree = new IndexTree(N);
		Right test = new Right(N);
		System.out.println("test begin");
		for (int i = 0; i < testTime; i++) {
			int index = (int) (Math.random() * N) + 1;
			if (Math.random() <= 0.5) {
				int add = (int) (Math.random() * V);
				tree.add(index, add);
				test.add(index, add);
			} else {
				if (tree.sum(index) != test.sum(index)) {
					System.out.println("Oops!");
				}
			}
		}
		System.out.println("test finish");
	}

}

IndexTree二维

二维数组从(1,1)位置到(i,j)位置的整块累加和填入help(i,j)的格子里面
假设原数组图中?位置的值被改变了,那么help数组变化范围?
行管的是0110001-0110100
列管的是0110001-0111000
行列管的范围内所有组合都受影响
算法拾遗三十五indexTree和AC自动机_第7张图片

// 测试链接:https://leetcode.com/problems/range-sum-query-2d-mutable
// 但这个题是付费题目
// 提交时把类名、构造函数名从Code02_IndexTree2D改成NumMatrix
public class IndexTree2D {
	private int[][] tree;
	private int[][] nums;
	private int N;
	private int M;

	public IndexTree2D(int[][] matrix) {
		if (matrix.length == 0 || matrix[0].length == 0) {
			return;
		}
		N = matrix.length;
		M = matrix[0].length;
		tree = new int[N + 1][M + 1];
		nums = new int[N][M];
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < M; j++) {
				update(i, j, matrix[i][j]);
			}
		}
	}

	private int sum(int row, int col) {
		int sum = 0;
		for (int i = row + 1; i > 0; i -= i & (-i)) {
			for (int j = col + 1; j > 0; j -= j & (-j)) {
				sum += tree[i][j];
			}
		}
		return sum;
	}

	public void update(int row, int col, int val) {
		if (N == 0 || M == 0) {
			return;
		}
		int add = val - nums[row][col];
		nums[row][col] = val;
		for (int i = row + 1; i <= N; i += i & (-i)) {
			for (int j = col + 1; j <= M; j += j & (-j)) {
				tree[i][j] += add;
			}
		}
	}

	public int sumRegion(int row1, int col1, int row2, int col2) {
		if (N == 0 || M == 0) {
			return 0;
		}
		return sum(row2, col2) + sum(row1 - 1, col1 - 1) - sum(row1 - 1, col2) - sum(row2, col1 - 1);
	}

}

AC自动机

(实质)前缀树+KMP
理解:假设有一个字典里面放着若干个敏感词,然后有一个大文章,AC自动机就是将大文章包含的每一个敏感词都收集到不能漏掉,并且将大文章包含了哪些敏感词都告诉我。
我们先将注意力放在敏感词上(“abc”,“bkf”,“abcd”),首先将敏感词的前缀树建立出来:
算法拾遗三十五indexTree和AC自动机_第8张图片
然后再对前缀树做升级,给前缀树做fail指针(做宽度优先遍历):
头节点的fail指针指向null,头节点的下一层的所有节点的fail指针指向头节点
算法拾遗三十五indexTree和AC自动机_第9张图片
假设b路径下面的节点为x节点,则需要找x的父节点的fail指针指向的节点(这里是头节点)有没有直接指向b方向的路【此处没有】再找头节点的fail指针看有没有指向b方向的路,发现也没有,则让x节点的fail指针指向头节点,一直宽度优先遍历找下去得到:
算法拾遗三十五indexTree和AC自动机_第10张图片
算法拾遗三十五indexTree和AC自动机_第11张图片
算法拾遗三十五indexTree和AC自动机_第12张图片
算法拾遗三十五indexTree和AC自动机_第13张图片
总结:
1、头节点的fail指针指向null
2、头节点的子节点的fail指针都指向头节点
3、如果对于某个节点x它的父节点通过路径@指向x,然后父亲节点的fail指针为某个节点甲,如果甲有路径@则x的fail指针直接指向过去。
算法拾遗三十五indexTree和AC自动机_第14张图片
fail指针含义:
算法拾遗三十五indexTree和AC自动机_第15张图片
有如上图敏感词信息,首先知道c路径下的一个x节点的fail指针是指向【cde】路径下c下的y节点,x节点从上到下匹配到敏感词abcde中的abc,但是在匹配d的时候失败了,下面所有字符串中哪一个字符串的前缀,它一定跟我必须以c结尾的后缀的一样,且是最长的,如下图abcd中d的fail指针是指向cde中的d节点,它没有去找de而是找的cde。
算法拾遗三十五indexTree和AC自动机_第16张图片
算法拾遗三十五indexTree和AC自动机_第17张图片

如上图没画虚线的fail指针都指向头节点,匹配大文章的过程中匹配到e位置后则匹配不上了,则需要通过e的fail指针来到必须以e结尾最长的前缀保留(cde),我准确的跳到有可能配到敏感词的下一个最可能的最近的位置,保证自己淘汰每一步都如此的小心,跳到第二个e位置,发现仍然匹配不上,然后再跳到第三个e位置。
第一层节点fail指针为什么指向头节点:
是因为如果a匹配到f的时候没找到任何节点可以匹配了,则可以通过fail指针跳到头节点重新找路径【相当于a不要了直接从f开始重新找】
算法拾遗三十五indexTree和AC自动机_第18张图片

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class AC {

	// 前缀树的节点
	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;
		}

		//build方法连fail指针
		public void build() {
			//准备队列做宽度优先遍历
			Queue<Node> queue = new LinkedList<>();
			//将头节点加入队列
			queue.add(root);
			Node cur = null;
			Node cfail = null;
			//由父来设置所有子的fail指针,当父弹出时设置关联子的fail指针
			while (!queue.isEmpty()) {
				// 某个父亲,cur
				cur = queue.poll();
				//考察所有的路(所有儿子节点)
				for (int i = 0; i < 26; i++) { // 所有的路
					// cur -> 父亲  如果有i号儿子,必须把i号儿子的fail指针设置好!
					if (cur.nexts[i] != null) { // 如果真的有i号儿子
						//先将孩子的fail指针设置成root
						cur.nexts[i].fail = root;
						//父亲的fail指针指向cfail,第一步跳转
						cfail = cur.fail;
						//如果父亲的fail指针不为null
						while (cfail != null) {
							//如果父亲的fail指针不为空,且有i方向的儿子
							if (cfail.nexts[i] != null) {
								//则让当前节点的儿子的fail指针指过去
								cur.nexts[i].fail = cfail.nexts[i];
								break;
							}
							//如果没有i方向的儿子则再往fail指针方向挑
							cfail = cfail.fail;
						}
						queue.add(cur.nexts[i]);
					}
				}
			}
		}

		// 大文章:content,在遍历每个单词的时候都顺着fail指针走一遍看是否有end串,有则收集
		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;
				//注意此处cur是没动的,而是通过follow去捞取的数据
				follow = cur;
				//来到任何节点过一圈fail指针
				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);
		}
	}

}

首先大文章的长度是N,所有匹配串长度M

大文章一定会过一遍,时间复杂度O(N)。

所有匹配串被用来做了AC自动机,一个节点最多一条fail指针,并且根据代码来看:

		while (follow != root) {
				if (follow.end == -1) {
					break;
				}
				{ // 不同的需求,在这一段{ }之间修改
					ans += follow.end;
					follow.end = -1;
				} // 不同的需求,在这一段{ }之间修改
				follow = follow.fail;
			}

每一条fail指针最多走一遍,因为沿途的follow.end都会被设置为-1。

一旦被设置成-1,那么这条fail指针的链就不会继续跳转了。

ac自动机节点数:M

fail指针总数目:M

如果在ac自动机上往下走的代价被算进了遍历大文章的过程里。

所以时间复杂度O(N+M)。

如果想统计各匹配串的匹配次数,是不是不设置endUse来做次数累加。

如果是的话对均摊、最坏时间复杂度有没有影响。

如果统计匹配串匹配次数的话有没有AC自动机之外别的方法呢?

如果统计各匹配串的匹配次数,那么就不能让沿途的follow.endUse被设置为true。

因为一旦标记true,那么该匹配串就被命中过了,那么下次这个字符串可能就不会随着fail指针转圈被再次统计到了。

但是如果不让沿途的follow.endUse被设置为true,那么复杂度会因此升高的,不再是O(N+M),而是O(N*fail指针圈的平均长度)

“如果统计匹配串匹配次数的话有没有AC自动机之外别的方法呢?”

你可以继续用ac自动机。因为fail指针圈的平均长度,一般情况除非刻意构造,否则不会太长。

或者,你先用ac自动机,看看哪些串被匹配到了,但是不统计次数。

然后每一个串,开一个线程去跑kmp算法,单独统计这个串被大文章引用了多少次。利用多线程来搞。

但是用ac自动机,效率也不会低到哪去的。除非你刻意构造fail指针圈很长的例子。

你可能感兴趣的:(算法块,算法)