后缀数组的应用:在哪个位置插入字符串使得字典序最大

news2025/6/20 1:39:20

题目描述

给定两个字符串 str1str2,想把 str2 整体插入到 str1 中某个位置,形成最大的字典序,返回字典序最大的结果。其中 str1 长度为 N N Nstr2 长度为 M M M,且 N > > M N >> M N>>M

思路分析

暴力解:尝试将 str2 插入到 str1 的每个位置,每次都会拼出一个 ( N + M ) (N+M) (N+M) 长度的字符串,然后和之前的字符串比较字典序,记录较大的字典序,所以时间复杂度为 O ( N ∗ ( N + M ) ) O(N * (N + M)) O(N(N+M)),而因为 N > > M N >> M N>>M,所以时间复杂度就是 O ( N 2 ) O(N^2) O(N2)

而使用DC3算法最终能将复杂度优化成 O ( N + M ) + O ( M 2 ) O(N + M) +O(M^2) O(N+M)+O(M2)

DC3算法:如果 「str1 从0出发的后缀串的字典序」 ≥ \ge str2从0出发的后缀串的字典序」,则str2没有必要插在 str1的0位置之前。同理,如果 「str1 i i i 出发的后缀串的字典序」 ≥ \ge str2 i i i 出发的后缀串的字典序」,则str2没有必要插在 str1 i i i 位置之前。

如果 str1 的每个位置开头的后缀串的字典序都是大于 str2的0位置开头的后缀串的字典序,则str2 直接插入到 str1后面即可,整个字符串就是 str1 + str2

如果 str1 i i i 位置开头的后缀串的字典序是小于str2 的 0 位置开头的后缀串的字典序的,那么只能说str2 可以插在 i i i 位置之前,也可以插在 i i i位置之后,但 i i i 之前的位置这个插入点是不可忽略的。

举两个例子:

  • 例1
str1:......993....     
 		    i
str2:994

此时 i i i 位置开头的后缀串小于 str2 的字典序,但是str2应该插入到 ( i + 2 ) (i+2) (i+2) 位置处,整体变成 …999943… 才是最优,这种情况下str2并不是插入到 i i i 位置之前。

  • 例2
str1: ......431......
str2: 994

此时 i i i 位置开头的后缀串小于 str2 的字典序,str2应该插入到 i i i 位置前面,整体变成 …994431… 才是最优,这种情况下str2就是插入到 i i i 位置之前。

也就是说,对于 i i i 位置来说,找到了最左的可能性位置,即 ( i − 1 ) (i-1) (i1) i i i 之间,而它的最右的可能性位置是从 i i i 位置开头的情况下,到哪个位置的时候才能和 str2 分出大小。

举个例子:

str1: ...... 9  9   9    3 ...
			 i i+1 i+2  i+3
str2:9994

i i i 位置开始,到 ( i + 3 ) (i+3) (i+3)位置才和 str2 分出大小,对于 i i i 位置来说,最左的可能性位置就是 ( i − 1 ) (i-1) (i1) i i i 之间,最右的可能性位置就是 ( i + 2 ) (i+2) (i+2) ( i + 3 ) (i+3) (i+3) 之间,所以 str2 要插入 ( i + 2 ) (i+2) (i+2) 位置后面才最优。

请添加图片描述
假设str2长度为 M M M,则 str1 从最左到最右的可能性的长度最多为 M M M,所以每次插入之后需要关心的字符串长度最多是 2 M 2M 2M,比较该长度的字符串的字典序,时间复杂度为 O ( M 2 ) O(M^2) O(M2)

流程梳理:

  1. 建立str1 i i i 位置出发的后缀串与str2字符串的字典序比较的机制;【DC3算法】
  2. 依次尝试str1从0位置开始的后缀串与str2大小的比较,直到 i i i 位置与 str2 的大小区分出来,此时知道了最左的可能性;【 O ( N ) O(N) O(N) 复杂度】
  3. str1 i i i 位置开始,找到是在哪个位置和 str2 分出胜负的,此时找到了最右的可能性;【 O ( M ) O(M) O(M) 复杂度】
  4. 截取最左可能性到最右可能性的局部串,找到最大的字典序,就能知道 str2 应该插入到哪个位置了。【 O ( M 2 ) O(M^2) O(M2) 复杂度】

str1str2 字符串之间加一个小的ASCII码字符,使其拼成一个长的字符串即str1 + 小ASCII码字符 + str2,对这个字符串使用DC3算法。

在DC3算法生成后缀数组详解一文中DC3算法的模板要求最小值>=1,假设str1 = [3, 1, 2],str2 = [1, 2],先给每个数值+2,就变成了 [5, 3, 4] 和 [3, 4],新增一个1将两个数组进行区分,即合成一个数组变成[5, 3, 4, 1(新增的值), 3, 4]。之所以用1来区分,是因为模板要求最小值>=1,而区分两个字符串的时候要新增一个超小的ASCII码字符,为了符合模板要求,它不能为0,所以用1来作区分,此时其他的值就要从2开始。也就是说,如果数组中的最小值是10,要将其对应成2,那么所有数都要-8;如果数组中的最小值是7,要将其对应成2,则所有数都要-5,这样一来,就可以用1来隔开两个数组。

代码实现

public class InsertS2MakeMostAlphabeticalOrder {

	// 暴力方法:尝试在每一个位置插入 O(N^2)
	public static String right(String s1, String s2) {
		if (s1 == null || s1.length() == 0) {
			return s2;
		}
		if (s2 == null || s2.length() == 0) {
			return s1;
		}
		String p1 = s1 + s2;
		String p2 = s2 + s1;
		String ans = p1.compareTo(p2) > 0 ? p1 : p2;
		for (int end = 1; end < s1.length(); end++) {
			String cur = s1.substring(0, end) + s2 + s1.substring(end);
			if (cur.compareTo(ans) > 0) {
				ans = cur;
			}
		}
		return ans;
	}

	// 正式方法 O(N+M) + O(M^2)
	// N : s1长度
	// M : s2长度
	public static String maxCombine(String s1, String s2) {
		if (s1 == null || s1.length() == 0) {
			return s2;
		}
		if (s2 == null || s2.length() == 0) {
			return s1;
		}
		char[] str1 = s1.toCharArray();
		char[] str2 = s2.toCharArray();
		int N = str1.length;
		int M = str2.length;
		//找到两个数组的最小值和最大值
		int min = str1[0];
		int max = str1[0];
		for (int i = 1; i < N; i++) {
			min = Math.min(min, str1[i]);
			max = Math.max(max, str1[i]);
		}
		for (int i = 0; i < M; i++) {
			min = Math.min(min, str2[i]);
			max = Math.max(max, str2[i]);
		}
		int[] all = new int[N + M + 1]; //新增了1个位置做两个数组的隔断
		int index = 0;
		for (int i = 0; i < N; i++) { //arr数组左边放str1数组的值,按照前文描述,要将最小数对应成2,其他数值依次进行调整
			all[index++] = str1[i] - min + 2; 
		}
		all[index++] = 1; //arr数组中间,人为增加了一个1
		for (int i = 0; i < M; i++) { //arr数组右边放str2数组的值,依然要按照要求对数值进行调整
			all[index++] = str2[i] - min + 2;
		}
		//调用DC3算法
		DC3 dc3 = new DC3(all, max - min + 2);
		int[] rank = dc3.rank;
		int comp = N + 1; //arr数组中str2数组开始的位置
		for (int i = 0; i < N; i++) {
			if (rank[i] < rank[comp]) { //如果在str1中找到了某个位置出发的后缀串的字典序 < str2的字典序
				int best = bestSplit(s1, s2, i); //选择最好的插入位置
				return s1.substring(0, best) + s2 + s1.substring(best);
			}
		}
		//如果str1中一直没有找到字典序 < str2的,直接插入到str1最后
		return s1 + s2;
	}

	public static int bestSplit(String s1, String s2, int first) {
		int N = s1.length();
		int M = s2.length();
		int end = N;
		for (int i = first, j = 0; i < N && j < M; i++, j++) {
			if (s1.charAt(i) < s2.charAt(j)) {
				end = i;
				break;
			}
		}
		String bestPrefix = s2;
		int bestSplit = first;
		for (int i = first + 1, j = M - 1; i <= end; i++, j--) {
			String curPrefix = s1.substring(first, i) + s2.substring(0, j);
			if (curPrefix.compareTo(bestPrefix) >= 0) {
				bestPrefix = curPrefix;
				bestSplit = i;
			}
		}
		return bestSplit;
	}

	public static class DC3 {

		public int[] sa;

		public int[] rank;

		public DC3(int[] nums, int max) {
			sa = sa(nums, max);
			rank = rank();
		}

		private int[] sa(int[] nums, int max) {
			int n = nums.length;
			int[] arr = new int[n + 3];
			for (int i = 0; i < n; i++) {
				arr[i] = nums[i];
			}
			return skew(arr, n, max);
		}

		private int[] skew(int[] nums, int n, int K) {
			int n0 = (n + 2) / 3, n1 = (n + 1) / 3, n2 = n / 3, n02 = n0 + n2;
			int[] s12 = new int[n02 + 3], sa12 = new int[n02 + 3];
			for (int i = 0, j = 0; i < n + (n0 - n1); ++i) {
				if (0 != i % 3) {
					s12[j++] = i;
				}
			}
			radixPass(nums, s12, sa12, 2, n02, K);
			radixPass(nums, sa12, s12, 1, n02, K);
			radixPass(nums, s12, sa12, 0, n02, K);
			int name = 0, c0 = -1, c1 = -1, c2 = -1;
			for (int i = 0; i < n02; ++i) {
				if (c0 != nums[sa12[i]] || c1 != nums[sa12[i] + 1] || c2 != nums[sa12[i] + 2]) {
					name++;
					c0 = nums[sa12[i]];
					c1 = nums[sa12[i] + 1];
					c2 = nums[sa12[i] + 2];
				}
				if (1 == sa12[i] % 3) {
					s12[sa12[i] / 3] = name;
				} else {
					s12[sa12[i] / 3 + n0] = name;
				}
			}
			if (name < n02) {
				sa12 = skew(s12, n02, name);
				for (int i = 0; i < n02; i++) {
					s12[sa12[i]] = i + 1;
				}
			} else {
				for (int i = 0; i < n02; i++) {
					sa12[s12[i] - 1] = i;
				}
			}
			int[] s0 = new int[n0], sa0 = new int[n0];
			for (int i = 0, j = 0; i < n02; i++) {
				if (sa12[i] < n0) {
					s0[j++] = 3 * sa12[i];
				}
			}
			radixPass(nums, s0, sa0, 0, n0, K);
			int[] sa = new int[n];
			for (int p = 0, t = n0 - n1, k = 0; k < n; k++) {
				int i = sa12[t] < n0 ? sa12[t] * 3 + 1 : (sa12[t] - n0) * 3 + 2;
				int j = sa0[p];
				if (sa12[t] < n0 ? leq(nums[i], s12[sa12[t] + n0], nums[j], s12[j / 3])
						: leq(nums[i], nums[i + 1], s12[sa12[t] - n0 + 1], nums[j], nums[j + 1], s12[j / 3 + n0])) {
					sa[k] = i;
					t++;
					if (t == n02) {
						for (k++; p < n0; p++, k++) {
							sa[k] = sa0[p];
						}
					}
				} else {
					sa[k] = j;
					p++;
					if (p == n0) {
						for (k++; t < n02; t++, k++) {
							sa[k] = sa12[t] < n0 ? sa12[t] * 3 + 1 : (sa12[t] - n0) * 3 + 2;
						}
					}
				}
			}
			return sa;
		}

		private void radixPass(int[] nums, int[] input, int[] output, int offset, int n, int k) {
			int[] cnt = new int[k + 1];
			for (int i = 0; i < n; ++i) {
				cnt[nums[input[i] + offset]]++;
			}
			for (int i = 0, sum = 0; i < cnt.length; ++i) {
				int t = cnt[i];
				cnt[i] = sum;
				sum += t;
			}
			for (int i = 0; i < n; ++i) {
				output[cnt[nums[input[i] + offset]]++] = input[i];
			}
		}

		private boolean leq(int a1, int a2, int b1, int b2) {
			return a1 < b1 || (a1 == b1 && a2 <= b2);
		}

		private boolean leq(int a1, int a2, int a3, int b1, int b2, int b3) {
			return a1 < b1 || (a1 == b1 && leq(a2, a3, b2, b3));
		}

		private int[] rank() {
			int n = sa.length;
			int[] ans = new int[n];
			for (int i = 0; i < n; i++) {
				ans[sa[i]] = i;
			}
			return ans;
		}

	}

	// for test
	public static String randomNumberString(int len, int range) {
		char[] str = new char[len];
		for (int i = 0; i < len; i++) {
			str[i] = (char) ((int) (Math.random() * range) + '0');
		}
		return String.valueOf(str);
	}

	// for test
	public static void main(String[] args) {
		int range = 10;
		int len = 50;
		int testTime = 100000;
		System.out.println("功能测试开始");
		for (int i = 0; i < testTime; i++) {
			int s1Len = (int) (Math.random() * len);
			int s2Len = (int) (Math.random() * len);
			String s1 = randomNumberString(s1Len, range);
			String s2 = randomNumberString(s2Len, range);
			String ans1 = right(s1, s2);
			String ans2 = maxCombine(s1, s2);
			if (!ans1.equals(ans2)) {
				System.out.println("Oops!");
				System.out.println(s1);
				System.out.println(s2);
				System.out.println(ans1);
				System.out.println(ans2);
				break;
			}
		}
		System.out.println("功能测试结束");

		System.out.println("==========");

		System.out.println("性能测试开始");
		int s1Len = 1000000;
		int s2Len = 500;
		String s1 = randomNumberString(s1Len, range);
		String s2 = randomNumberString(s2Len, range);
		long start = System.currentTimeMillis();
		maxCombine(s1, s2);
		long end = System.currentTimeMillis();
		System.out.println("运行时间 : " + (end - start) + " ms");
		System.out.println("性能测试结束");
	}
}

扩展

例如数组 [100万,5, 90万,10亿],这个数组依然可以求解后缀数组,也可以用DC3算法做,但是因为最大值太大,需要的桶就太多,会导致DC3算法的常数时间变得很大。

可以怎么做呢?进行离散化——将5对应成1,90万对应成2,100万对应成3,10亿对应成4,那么 [100万,5, 90万,10亿] 的后缀数组等同于 [3, 1, 2, 4] 的后缀数组。

所以如果一个数组中的数千差万别,可以将其对应成一个较窄的域,然后调用DC3算法,可以减少常数时间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/426373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【排序算法 下】带你手撕常见排序 (冒泡,快排,归并排序) (动图详解)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; “只要有花可开&#xff0c;就不允许生命与黯淡为伍。” 前言&#xff1a; 承接上篇&#xff0c;继续带大家手撕常见排序算法&#xff0c;这次讲剩余的两类&#xff1a;交换排序和归并排序。 注&#xff1a;…

C++——模板初阶与泛型编程

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;引例&#x1f337;函数模板&#x1f33a;函数模板的概念&#x1f33a;函数模板的格式&#x1f337;函数模板的原理&#x1f337;函数模板的实例化&#x1f33a;隐式实例化&#x1f33a;显式实例化&#x1f33a…

Maven安装

目录 1.Maven安装 1.1下载 1.2 安装步骤 1、解压 apache-maven-3.6.1-bin.zip&#xff08;解压即安装&#xff09; 2、配置本地仓库 3、配置阿里云私服 4、配置环境变量 1.3 安装检测 1.Maven安装 认识了Maven后&#xff0c;我们就要开始使用Maven了&#xff0c;那么首…

基于OpenCv的图像分割(分水岭算法)

文章目录图像分割distanceTransform()connectedComponents()watershed()查看图像的矩阵图像分割 图像分割对于图像处理和计算机视觉领域非常重要&#xff0c;可以用于对象识别、图像分析、图像压缩等应用。 注意&#xff1a;通常我们把前景目标的灰度值设为255&#xff0c;即白…

网络原理与网络通信

目录 网络互连原理 网络通信 IP地址和端口号 网络协议 五元组 协议分层 OSI七层模型 TCP/IP五层模型 封装和分用 网络互连原理 计算机在最开始的时候是没有网络的&#xff0c;每个计算机之间相互独立。这样处理信息就非常的麻烦&#xff0c;为了能够更高效的利用计算…

一个基于Java线程池管理的开源框架Hippo4j实践

文章目录概述定义线程池痛点功能框架概览架构部署Docker安装二进制安装运行模式依赖配置中心接入流程个性化配置线程池监控无中间件依赖接入流程服务端配置三方框架线程池适配拒绝策略自定义概述 定义 Hippo4j 官网地址 https://hippo4j.cn/ 最新版本1.5.0 Hippo4j 官网文档地…

硬件系统工程师宝典(17)-----你的PCB符合工艺要求吗?

各位同学大家好&#xff0c;欢迎继续做客电子工程学习圈&#xff0c;今天我们继续来讲这本书&#xff0c;硬件系统工程师宝典。上篇我们说到PCB设计中板子要符合EMC&#xff0c;信号的走线要平顺&#xff0c;信号回流阻抗尽量小。今天我们开始看看板子在生产制造时的工艺问题。…

【安全防御】防火墙(二)

目录 1、防火墙如何处理双通道协议 2、防火墙如何处理nat 3、防火墙支持哪些NAT&#xff0c;主要应用的场景是什么&#xff1f; 4、当内网PC通过公网域名解析访问内网服务器的时候&#xff0c;会存在什么问题&#xff0c;如何解决&#xff1f;请详细说明 5.防火墙使用VRRP…

面试题总结-JS

文章目录一、JS 系列1、原型、原型链2、闭包3、this指向4、call、 apply、 bind 的作用与区别&#xff1f;5、数组扁平化6、var、let、const 区别7、对称加密和不对称加密的区别8、js 的栈和堆9、对象的深拷贝和浅拷贝10、浏览器的事件循环机制11、宏任务和微任务12、script 标…

StringBuilder、StringBuffer、String的区别

StringBuilder与StringBuffer的append方法源码分析 #mermaid-svg-N8145OzAyMWzlewt {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-N8145OzAyMWzlewt .error-icon{fill:#552222;}#mermaid-svg-N8145OzAyMWzlewt .er…

C#基础学习--泛型

目录 C#中的泛型 泛型类 声明泛型类 创建构造函数 创建变量和实例 类型参数的约束 Where 子句 泛型方法 声明泛型方法 ​编辑 调用泛型方法 扩展方法和泛型类 泛型结构 泛型委托 泛型接口 协变 逆变 接口的协变和逆变 C#中的泛型 泛型允许我们声明 类型参数化 的代码&…

Jetpack Compose大师乘势而上,创建引人入胜和直观的UI;实用技巧和技术

简述 Jetpack Compose 是 Android 上的一种全新的 UI 工具箱&#xff0c;旨在简化 Android UI 开发流程&#xff0c;提高开发效率和应用性能&#xff0c;并且提供更直观、更灵活、更强大的 UI 定义方式。 Jetpack Compose 提供了一套新的声明式 UI 编程模型&#xff0c;采用 …

【Redis】多级缓存(nginx缓存、redis缓存及tomcat缓存)

【Redis】多级缓存 文章目录【Redis】多级缓存1. 传统缓存的问题2. 多级缓存方案2.1 JVM进程缓存2.1.1 本地进程缓存2.1.2 Caffeine2.2 Nginx缓存2.2.1 准备工作2.2.2 请求参数处理2.2.3 nginx发送http请求tomcat2.2.3.1 封装http查询函数2.2.3.2 使用http函数查询数据2.2.4 ng…

Huffman 编码

1.Huffman编码 1952年提出一种编码方法&#xff0c;该方法完全依据字符出现概率来构造异字头的平均长度最短的码字&#xff0c;有时称之为最佳编码&#xff0c;一般就叫做Huffman编码(有时也称为霍夫曼编码)。 2.Huffman树 树是一种重要的非线性数据结构&#xff0c;它是数据元…

​2023年十大目标检测模型!

“目标检测是计算机视觉中最令人兴奋和具有挑战性的问题之一&#xff0c;深度学习已经成为解决该问题的强大工具。”—Dr. Liang-Chieh Chen目标检测是计算机视觉中的基础任务&#xff0c;它涉及在图像中识别和定位目标。深度学习已经革新了目标检测&#xff0c;使得在图像和视…

【CV大模型SAM(Segment-Anything)】真是太强大了,分割一切的SAM大模型使用方法:可通过不同的提示得到想要的分割目标

目录前言安装运行环境SAM模型的使用方法导入相关库并定义显示函数导入待分割图片使用不同提示方法进行目标分割方法一&#xff1a;使用单个提示点进行目标分割方法二&#xff1a;使用多个提示点进行目标分割方法三&#xff1a;用方框指定一个目标进行分割方式四&#xff1a;将点…

文件操作和IO—javaEE

文章目录1.文件1.1文件系统的结构1.2java中的文件操作&#xff08;metadata的操作&#xff09;2.io操作2.1定义2.2io划分2.3java的io流之输入流2.4java的io流之输出流1.文件 文件包含数据本身和文件的头信息&#xff08;metadata&#xff09;&#xff0c;文件的头信息包括文件…

VSCode的C/C++编译调试环境搭建(亲测有效)

文章目录前言1.安装VSCode和mingw642.配置环境变量3.配置VSCode的运行环境3.1设置CodeRunner3.2设置C/C4.调试环境配置前言 这片博客挺早前就写好了&#xff0c;一直忘记发了&#xff0c;写这篇博客之前自己配的时候也试过很多博客&#xff0c;但无一例外&#xff0c;都各种js…

SpringBoot(4)整合数据源

SpringBoot整合数据源数据层解决方案数据源技术持久化技术数据库技术NoSQL整合Redis整合MongDB整合ES数据层解决方案 MySQL数据库与MyBatisPlus框架&#xff0c;后面又用了Druid数据源的配置&#xff0c;所以现在数据层解决方案可以说是MysqlDruidMyBatisPlus。而三个技术分别…

一文彻底了解派克Parker无铁芯/有铁芯直线电机及其应用

一、什么是直线电机&#xff1f; 直线电机是一种将电能直接转换成直线运动机械能&#xff0c;而不需要任何中间转换机构的传动装置。它可以看成是一台旋转电机按径向剖开&#xff0c;并展成平面而成。 二、直线电机的特点 直线电机类似于一台旋转电机解剖摊开来进行运转。在一…