第一部分语言基础
基础
数组
数组的定义
用来存储固定大小的同类型元素
特点:
1.本身就是引用数据类型,但数组中的元素可以是任意数据类型
2.数组会在内部开辟一整块连续的空间,占据的大小取决于数组的长度和数组中的元素类型
3.数组中的元素在内存了是依次紧密连续的
4.数组一旦初始化完成,那么数组的长度就不能修改
5.可以直接通过下标的方式调用指定位置的元素-数组名中引用的是这块连续空间的首地址
6.数组的索引是从0开始的
数组的声明
一维数组 int[] arr; 二维数组 int[][] arr;
初始化
静态:int[] arr = new int[]{1,2,3,4,5,6};
动态:int[] arr = new int[5];
遍历数组
一维数组 for循环for(int i = 0; i < arr.length; i++) 或 for(int k : arr)
二维数组 两层for循环
java.util.Arrays类
可以方便操作数组,提供的所有方法都是静态的
Arrays.fill对数组进行赋值 Arrays.sort对数组进行排序(默认升序)
List(链表)
List集合实例化
步骤:
1.导入java.util包
2.通过ArrayList类创造一个List对象
3.格式:List<Integer> list = new List<>();
常用方法
ArrayList是一个可以动态修改的数组,于普通数组的区别是没有固定大小的限制,我们可以添加或删除元素
1.add(Object element): 像列表末尾添加指定元素
2.size(): 返回列表中的元素个数
3.get(int index): 返回列表中指定位置的元素,index从0开始
4.isEmpty(): 判断列表是否为空,为空返回true,负否则返回false
5.contains(): 如果列表包含指定元素,则返回true
6.remove(int index): 移除列表中指定位置的元素,并返回被删除的元素
List集合特点
1.元素的添加顺序于取出顺序是一致的,并且可以重复
2.每个元素都有其对应的舒徐索引(从0开始)
3.可以根据喜好存取容器中的元素
排序
数组排序
Arrays.sort(int[] arr)对数组arr进行排序(默认升序)
Integer[] arr = {1,2,3,4,5,1,5,6,2};
//升序
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
//降序
Arrays.sort(arr, new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2){
return o2 - o1;
}
});
System.out.println(Arrays.toString(arr));
//第二种模式,上面的简写
Integer[] arr = {1,2,3,4,5,1,5,6,2};
//升序
Arrays.sort(arr, (a,b)->a-b);
System.out.println(Arrays.toString(arr));
//降序
Arrays.sort(arr, (a,b)->b-a);
System.out.println(Arrays.toString(arr));
集合排序
Collections.sort(List<> arr)
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(4);
list.add(3);
list.add(4);
list.add(5);
list.add(4);
//升序
Collections.sort(list);
System.out.println(list);
//降序
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println(list);
//第二种模式,上面的简写
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(4);
list.add(3);
list.add(4);
list.add(5);
list.add(4);
//升序
Collections.sort(list, (a,b)->a-b);
System.out.println(list);
//降序
Collections.sort(list, (a,b)->b-a);
System.out.println(list);
Set集合(集合)
Set的定义
是一个不允许出现重复元素,并且无序的集合,主要有HashSet类
在判断重复元素的时候,Set集合会调用hashCode()和equal()的方法来实现
注重独一无二的性质,该体系集合可以知道某物是否已经存在于集合中,不会存储重复的元素
用于存储无序的元素,值不能重复
特点:
1.可以用来去重
2.元素无序
HashSet类
定义:
Set<Integer> set = new HashSet<>();
Set<String> set = new HashSet<>();
1.实现了Set接口
2.无序,疾病和记录插入地方顺序
3.不允许有重复元素的集合
4.谓语java.util包里面,需要引入
常用方法
Set<Integer> set = new HashSet<>();
boolean a = set.add(1);
boolean b = set.add(1);
System.out.println(a);//true
System.out.println(b);//false
Set<Integer> set = new HashSet<>();
boolean a = set.add(1);
boolean b = set.add(1);
System.out.println(set.size());//1
boolean c = set.add(2);
System.out.println(set.size());//2
System.out.println(a+" "+b); //true false
System.out.println(a+" "+c); //true true
Set<Integer> set = new HashSet<>();
set.add(1);
boolean a = set.remove(1);
boolean b = set.remove(5);
System.out.println(a+" "+b);//true false
Set<Integer> set = new HashSet<>();
set.add(1);
boolean a = set.contains(1);
boolean b = set.contains(5);
System.out.println(a+" "+b);//true false
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
System.out.println(set.size());//3
set.clear();
System.out.println(set.size());//0
Map(键值对集合)
Map集合实例化
步骤
1.导包,导入java.util.Map
2.通过HashMap实现类创造对象
3.语法格式:
Map<引用数据类型,引用数据类型> map = new HashMap<>();
特点:
1.HashMap是一个散列表,它存储的内容师键值对(key-value)映射。
2.HashMap实现了Map接口,根据键的HashMap值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步
3.HashMap是无序的,既不会记录插入的顺序
常用方法
HashMap<Integer,Integer> map = new HashMap<>();
map.put(key, value);//在 Map 中添加一对键值对,如果 key 已存在则会覆盖原有的 value 值
map.get(key);//根据 key 获取 value,如果 key 不存在则返回 null。
int size = map.size();//获取 Map 的大小(即键值对的数量)
Set<map.Entry<String, Integer>> entrySet = map.entrySet();
//作用是为了遍历Map集合
//返回 Map 中所有键值对组成的 Set 集合,其中每个元素都是一个 Map.Entry 对象,包含 key 和 value 两部分。
// 遍历 entrySet,获取每个键值对并处理
for (Map.Entry<String, Integer> entry : entrySet) {
String key = entry.getKey();
Integer value = entry.getValue();
// 处理键值对
System.out.println("Key: " + key + ", Value: " + value);
}
int value = map.getOrDefault(key,defaultVaule);
//如果存在第一个参数(key)对应的Value,那么九江Value赋值给value,反之将defaulValue赋给value
Stack(栈)
Stack的实例化
步骤:
1.导包,导入java.util.Stack
2.实例化对象,格式:
Stack<Integer> stack = new Stack<>();
特点:
先进后出
常用方法
Stack<Object> stack = new Stack<>();
stack.push(Object element); // 向栈中压入元素
stack.pop(); // 从栈中弹出(移除)并返回栈顶元素
stack.peek(); // 返回栈顶元素,但不弹出(移除)
stack.isEmpty(); // 检查栈是否为空
int element = stack.get(index); // 获取索引为1的元素,即栈顶元素下面的元素
element = stack.elementAt(index); // 获取索引为2的元素,即栈顶元素下面的元素又下面的元素
int size = stack.size(); // 获取栈中元素的个数
// 在栈中查找元素o的位置(从栈顶往下数),如果,没有这个元素,则返回1
int position = stack.search(Object o);
Queue(队列)
队列的实例化
步骤:
1.导包,导入java.util.Queue;
2.通过LinkList类创造对象
Queue<Integer> queue = new LinkedList<>();
特点:
先进先出
常用方法
queue.add(Objects element);//添加元素
queue.poll(); //取出元素
queue.peek(); //获取头部元素,不取出
queue.isEmpty(); //判断队列是否为空
//将元素添加到队列的末尾,并返回 true。如果队列已满,该方法会立即返回 false。
queue.offer(Objects o);
//移除并返回队列的头部元素。如果队列为空,该方法会抛出 NoSuchElementException 异常
queue.remove();
//element():返回队列的头部元素,但不会将其移除。如果队列为空,该方法会抛出 NoSuchElementException 异常。
queue.element();
//移除队列中的所有元素。
queue.clear();
例题
数位排序
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[][] arr = new int[n][2];
for (int i = 0; i < n; i++){
arr[i][0] = i+1;
String s = arr[i][0]+"";
for (int k = 0; k < s.length(); k++){
arr[i][1] += s.charAt(k) - '0';
}
}
Arrays.sort(arr, (o1,o2)->(o1[1] == o2[1]?o1[0]-o2[0]:o1[1]-o2[1]));
System.out.println(arr[m-1][0]);
}
}
顺子日期
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
HashMap<Integer,Integer> map = new HashMap<>();
map.put(1,31);
map.put(2,29);
map.put(3,31);
map.put(4,30);
map.put(5,31);
map.put(6,30);
map.put(7,31);
map.put(8,31);
map.put(9,30);
map.put(10,31);
map.put(11,30);
map.put(12,31);
int count = 0;
for (int i = 1; i <= 12; i++){
for (int k = 1; k <= map.get(i); k++){
String month = String.format("%02d", i);
String day = String.format("%02d", k);
String str = "2022"+month+day;
if (str.indexOf("012")!=-1||str.indexOf("123")!=-1||str.indexOf("234")!=-1||str.indexOf("345")!=-1||str.indexOf("456")!=-1||str.indexOf("567")!=-1||str.indexOf("678")!=-1||str.indexOf("789")!=-1){
count++;
}
}
}
System.out.println(count);
}
}
小明和完美序列
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++)
arr[i] = sc.nextInt();
Arrays.sort(arr);
int ans = 1; // 连续的长度
int count = 0;//要删除的个数
HashMap<Integer, Integer> map = new HashMap<>();
int temp = arr[0];
for (int i = 1; i < n; i++){
int m = arr[i];
if (m == temp){
ans++;
}else {
map.put(temp, ans);
ans = 1;
}
temp = m;
}
map.put(temp, ans);
ans = 1;
for (Map.Entry<Integer, Integer> entry : map.entrySet()){
Integer k = entry.getKey();//数
Integer v = entry.getValue();//长度
if (k > v) count += v;
else if(k < v) count += v - k;
}
System.out.println(count);
}
}
第二部分基础算法
时间复杂度
排序(排序算法)
1.冒泡排序(时间复杂度o(n^2))
概念
通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
步骤
- 比较相邻两个数据如果。第一个比第二个大,就交换两个数
- 对每一个相邻的数做同样1的工作,这样从开始一队到结尾一队在最后的数就是最大的数。
- 针对所有元素上面的操作,除了最后一个。
- 重复1~3步骤,直到顺序完成
可视化
代码实现
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {1,3,5,4,2,6,8,285,32,56,52,1};
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
2.选择排序(时间复杂度o(n^2))
概念
基本原理是每一次从待排序的数组里找到最小值(最大值)的下标,然后将最小值(最大值)跟待排序数组的第一个进行交换,然后再从剩余的未排序元素中寻找最(大)元素,然后放到已排序的序列的末尾。反复的进行这样的过程直到待排序的数组全部有序。
步骤
1.在待排序的序列中找到最小的元素,将最小元素与排序序列中首位元素交换位置
2.从剩余的未排序序列中,继续找出一个最小元素,将最小元素与排序好的序列的下一个位置进行交换
3.重复步骤 2 的工作,直至排序完成
可视化
代码实现
import java.util.Arrays;
public class selectSort {
public static void main(String[] args) {
int[] arr ={29,10,14,37,14,2,8,1,7,6,5};
for (int i = 0; i < arr.length - 1; i++) {
//注意min的取值
int min = i;
//j=i;意味着i之前的数都是有序的
for (int j = i; j < arr.length; j++) {
if (arr[j] < arr[min]){
min = j;
}
}
//交换,每一次循环的i都是未排序数据的第一个
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
System.out.println(Arrays.toString(arr));
}
}
3.插入排序(时间复杂度o(n^2))
概念
基本思想是将未排序部分的每个元素按照大小插入到已排序部分的适当位置。
步骤
- 从第一个元素开始,该元素可以认为已经被排序
- 取下一个元素tem,从已排序的元素序列从后往前扫描
- 如果该元素大于tem,则将该元素移到下一位
- 重复步骤3,直到找到已排序元素中小于等于tem的元素
- tem插入到该元素的后面,如果已排序所有元素都大于tem,则将tem插入到下标为0的位置
- 重复步骤2~5
可视化
代码示例
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 4, 2, 6, 8, 285, 32, 56, 52, 1};
for (int i = 0; i < arr.length - 1; ++i)
{
int end = i;//记录有序序列最后一个元素的下标
int temp = arr[end + 1];//待插入的元素
while (end >= 0)//单趟排
{
if (temp < arr[end]) {//比插入的数大就向后移
arr[end + 1] = arr[end];
end--;
} else {//比插入的数小,跳出循环
break;
}
}
//tem放到比插入的数小的数的后面
arr[end + 1] = temp;
}
System.out.println(Arrays.toString(arr));
}
}
4.快速排序(时间复杂度n*log(2)n)
概念
快速排序(Quick Sort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤
- 从数列中挑出一个元素,称为"基准"(pivot),通常选择第一个元素
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
可视化
动画一
动画二
静态演示
代码示例
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {30, 40, 60, 10, 20, 50};
quickSort(arr, 0, arr.length - 1);
// [20, 10, 30, 60, 40, 50]
// [10, 20, 30, 60, 40, 50]
// [10, 20, 30, 60, 40, 50]
// [10, 20, 30, 50, 40, 60]
// [10, 20, 30, 40, 50, 60]
// [10, 20, 30, 40, 50, 60]
}
//快速排序
public static void quickSort(int[] arr, int start, int end) {
//递归结束的标记
if (start < end) {
//把数组中第0个数字作为标准数
int stard = arr[start];
//记录需要排序的下标
int low = start;
int high = end;
//循环找比标准数大的数和标准数小的数
while (low < high) {
//如果右边数字比标准数大,下标向前移
while (low < high && arr[high] >= stard) {
high--;
}
//右边数字比标准数小,使用右边的数替换左边的数
arr[low] = arr[high];
//如果左边数字比标准数小
while (low < high && arr[low] <= stard) {
low++;
}
//左边数字比标准数大,使用左边的数替换右边的数
arr[high] = arr[low];
}
//把标准数赋给低所在的位置的元素
arr[low] = stard;
//打印每次排序后的结果
System.out.println(Arrays.toString(arr));
//递归处理所有标准数左边的数字(含标准数)
quickSort(arr, start, low);
//递归处理所有标准数右边的数字
quickSort(arr, low + 1, end);
}
}
}
5.归并排序(时间复杂度O(nlogn))
概念
是创建在归并操作上的一种有效的排序算法。算法是采用分治法的一个非常典型的应用,且各层分治递扫可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列:
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置:
- 重复步骤 3 直到某一指针达到序列尾:
- 将另一序列剩下的所有元素直接复制到合并序列尾
可视化
代码示例
public static int[] mergeSort(int[] arr, int l, int h){
if (l == h){
return new int[] {arr[l]};
}
int mid = (l+h)/2;
int[] leftArr = mergeSort(arr, l, mid);
int[] rightArr = mergeSort(arr, mid + 1, h);
int[] newArr = new int[leftArr.length+rightArr.length];
int m = 0, i = 0, j = 0;
while (i < leftArr.length && j < rightArr.length){
newArr[m++] = leftArr[i] < rightArr[j] ? leftArr[i++]:rightArr[j++];
}
while (i < leftArr.length)
newArr [m++] = leftArr[i++];
while (j < rightArr.length)
newArr [m++] = rightArr[j++];
return newArr;
}
6.桶排序 (时间复杂度与内置的排序有关)
概念
桶排序(Bucket sort) 是一种非比较的排序算法。桶排序采用了一些分类和分治的思想,把元素的值域分成若干段,每一段对应一个桶。在排序的时候,首先把每一个元素放到其对应的桶中,再对每一个桶中的元素分别排序,再按顺序把每个桶中的元素依次取出,合并成最终答案。
步骤
- 将值域分成若干段,每段对应一个桶
- 将待排序元素放入对应的桶中
- 将个桶内的元素进行排序
- 将桶中的元素依次取出
可视化
代码示例
public static void sort(int[] arr){
List[] buckets = new ArrayList[10];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<Integer>();
}
for (int value : arr) {//对每个数值进行分类
buckets[value / 1000].add(value);
}
for (List bucket : buckets) {
Collections.sort(bucket);
}
int k = 0;
for (List bucket : buckets) {
if (!bucket.isEmpty()) {
for (Object o : bucket) {
arr[k++] = (int) o;
}
}
}
}
7.希尔排序(时间复杂度o(n*1.3))
概念
实质上是采用分组插入的方法。先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。
这样当经过几次分组排序后,整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
希尔对记录的分组,不是简单地“逐段分割",而是将相隔某个“增量”的记录分成组。它并不能保证每趟排序至少能将一个元素放到其最终位置上。
步骤
1.先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…
2.当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成
可视化
示例代码
public static void shellSort(int[] arr, int n){
for (int gap = n / 2; gap > 0; gap /= 2){
for (int i = gap; i < n; i++){
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp){
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
8.基数排序(时间复杂度o(n*k))
概念
将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
步骤
- 确定数组中的最大元素有几位(MAX)(确定执行的轮数)
- 创建0~9个桶(桶的底层是队列),因为所有的数字元素都是由0~9的十个数字组成
- 依次判断每个元素的个位,十位至MAX位,存入对应的桶中,出队,存入原数组;
- 直至MAX轮结束输出数组。
可视化
示例代码
private static void radixSort(int[] arr) {
//待排序列最大值
int max = arr[0];
int exp;//指数
//计算最大值
for (int anArr : arr) {
if (anArr > max) {
max = anArr;
}
}
//从个位开始,对数组进行排序
for (exp = 1; max / exp > 0; exp *= 10) {
//分桶个数
ArrayList<Integer>[] lists = new ArrayList[10];
for (int i = 0; i < 10; i++) {
lists[i] = new ArrayList<>();
}
for (int k : arr){
lists[(k/exp)%10].add(k);
}
int index = 0;
for (ArrayList<Integer> i : lists){
for (Integer k : i){
arr[index] = k;
index++;
}
}
}
}
基础算法
时间复杂度
定义
时间复杂度是用来分析算法执行时间随着问题规模增大而增长的速度,它衡量了算法的执行效率
通常使用Big O表示法来表示时间复杂度,它描述了算法执行时间的上界。比如,一个算法的时间复杂度为O(n),表示随着问题规模n的增加,算法的执行时间线性增长。O(1)表示算法的执行时间是常数级别的与问题规模无关。
时间复杂度分析主要关注算法中占用主要时间的操作,例如循环、递归等。通过分析这些操作的执行次数可以确定算法的时间复杂度。
级别:时间复杂度可以分为几个级别,常见的有: 常数级别 (O(1))对数级别 (O(log n))线性级别(O(n)) 、线性对数级别 (O(n log n)) 、平方级别 (O(n2)) 、立方级别 ((n3)) 和指数级别(O(2^n)) 等。时间复杂度越低,算法执行速度越快。
数据范围反推时间复杂度
1.当题目数据 n<100 0(n^3)
2当题目数据 n≤1000 0(n2)或者O(n2)log(n)
3.当题目数据 n<100000 0(n)或者O(n)*log(n)
4.当题目数据 n≤1000000000 0(Vn)
枚举
定义
枚举算法我们也称之为穷举算法这种算法就是在解决问题的时候去使用所有的方式去解决这个问题会通过推理去考虑事件发生的每一种可能,最后推导出结果。
技巧
1.确定解的型
在进行枚举之前,我们要确定解的类型是什么,如果是求满足条件的数目,那我们就枚举每个变量,计算一共有多少个满足条件的数据。如果是满足条件区间的个数,那我们就枚举每个区间。
2.选择枚举的方法
常见的枚举方法有直接枚举法和递归枚举法。
3.判断是否满足条件
在枚举出一个解后,我们需要判断其是否是可行解。
模拟
定义
仅仅使用较简单的算法和数据结构的题目,模拟顾名思义,就是按照题目的要求,一步步写出代码。
特点:
模拟题目通常具有码量大、操作多、思路繁复的特点
过程
- 读题。读懂题目的意思,要知道题目想做什么。
- 建模。利用什么样的数据结构来实现。
- 代码实现。写出代码框架
- 调试、优化。
- 例题
例题 (DNA序列修正(模拟))
import java.util.*;
public class Main {
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
int n = sc.nextInt();
sc.nextLine();
char[] s1 = sc.next().toCharArray();
sc.nextLine();
char[] s2 = sc.next().toCharArray();
Map<Character, Integer> map = new HashMap<>();
map.put('A', 0);
map.put('C', 1);
map.put('G', 2);
map.put('T', 3);
int res = 0;
// 如果当前两个碱基相加不等于三 那就从当位置的下一个位置开始枚举 看后面有没有交换后两边都为三的 如果有就交换
for(int i = 0; i < n; i++) {
if(map.get(s1[i]) + map.get(s2[i]) != 3) {
for(int j = i + 1; j < n; j++) {
if(map.get(s1[i]) + map.get(s2[j]) == 3 && map.get(s1[j]) + map.get(s2[i]) == 3) {
char temp = s2[i];
s2[i] = s2[j];
s2[j] = temp;
break;
}
}
res++;
}
}
System.out.println(res);
}
}
递归
概念
递归,在数学与计算机科学中,是指在方法的定义中使用方法自身。也就是说,递归算法是一种直接或者间接调用自身方法的算法。简言之: 在定义自身的同时又出现自身的直接或间接调用。
注意: 递归必须要有一个退出的条件
递归算法解决问题的特点:
1.递归就是方法里调用自身。
2.在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
3.递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
4.在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等
总结
- 找到一种划分的方法
- 找到递推公式或者等价转换,这些都是父问题转化为求解子问题
- 找变化量:变化的通常要作为参数的出口
前缀和
概念
对于一个给定的数组 A,它的前缀和数组S 中S 表示从第 1个元素到第 个元素的总和。用公式表示为:
前缀和作用:在O(1)的时间求出数组任意区间的区间和
模板
int n = sc.nextInt();
int[] arr = new int[n];
long[] sum = new long[n+1];
for (int i = 1; i <= n + 1; i++){
sum [i] = sum[i - 1] + (long) arr[i - 1];
}
例题(大石头的搬运工)
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
stone[] a = new stone[100005];
for (int i = 1; i <= n; i++){
int w = sc.nextInt();
int p = sc.nextInt();
a[i] = new stone(w, p);
}
Arrays.sort(a,1 , n + 1, (o1, o2) -> o1.p - o2.p);
long[] cost1 = new long[100005];
long[] cost2 = new long[100005];
long sum = a[1].w;
for (int i = 2; i <= n; i++){
cost1[i] += cost1[i - 1] + sum*(a[i].p - a[i - 1].p);
sum += a[i].w;
}
sum = a[n].w;
for (int i = n - 1; i >= 1; i--){
cost2[i] += cost2[i + 1] + sum*(a[i+1].p - a[i].p);
sum += a[i].w;
}
long ans = Long.MAX_VALUE;
for (int i = 1; i <= n; i++){
ans = Math.min(ans, cost1[i] + cost2[i]);
}
System.out.println(ans);
}
}
class stone{
int w;
int p;
public stone(int w, int p) {
this.w = w;
this.p = p;
}
}
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
int[]a = new int[200005];
long[] b = new long[200005];
//模拟的情况是前2*k个a[]与前2*(k - 1) + 最后一个比较 选出最小的
//模拟的情况是前2*k个a[]与前2*(k - 2) + 最后一个 + 最后第二个比较 选出最小的
//模拟的情况是前2*k个a[]与前2*(k - 1) + 最后一个 + 最后第三个比较 选出最小的
while (t-->0){
int n = sc.nextInt();
int k = sc.nextInt();
long sum = 0;
for (int i = 1; i <= n; i++){
a[i] = sc.nextInt();
sum += a[i];
}
Arrays.sort(a, 1,n + 1);
for (int i = 1; i < n; i++) {
b[i] = b[i - 1] + a[i];
}
long back = 0;
long min = b[2 * k];
long ans = 0;
for (int i = 1; i <= k; i++){
back += a[n - i + 1];
ans = b[2 * (k - i)] + back;
min = Math.min(ans, min);
}
System.out.println(sum - min);
}
sc.close();
}
}
差分
概念
差分的主要用处在于:
快速将数组A 的区间l] 加 d (即: 把 AA(+1).....Ar这几个元素各加上 d)
一般的计算方法,需要对区间内所有元素进行遍历并分别加上 d。所以时间复杂度是 O(n).
若题目要求进行 m次这样的区间操作,则时间复杂度变为 O(m*n)。
而差分则可以将在原序列上的“区间操作” 转化为差分序列上的“单点操作”
因此,使用差分可以将单次区间操作的时间复杂度优化到 (1)m 次查询的总时间复杂度优化到 (m)
最后,若将差分序列转化为原序列,最终的时间复杂度是 O(n+m)
模板
public static void chaifenl(int[] arr, int l, int r, int d){
arr[l] += d;
arr[r + 1] -= d;
}
贪心算法
概念
贪心算法的基本思路
1.建立数学模型来描述问题
2.把求解的问题分成若干个子问题
3.对每一子问题求解,得到子问题的局部最优解
4.把子问题的解局部最优解合成原来解问题的一个解
总结:从局部最优做到全局最优
例题
最大的卡牌价值
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int k = scan.nextInt();
int a[] = new int[n];
int b[] = new int[n];
int c[] = new int[n];
long sum=0;
for (int i = 0; i < n; i++) {
a[i] = scan.nextInt();
sum += a[i];
}
for (int i = 0; i < n; i++) {
b[i] = scan.nextInt();
c[i] = b[i] - a[i];
}
Arrays.sort(c);
for (int i = n - 1; i >= 0 && k > 0; i--, k--) {
if (c[i] < 0) break;
sum += c[i];
}
System.out.println(sum);
scan.close();
}
}
珠宝的最大交替和
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
long ans =0;
if(a==1){
System.out.println(Math.abs(sc.nextLong()));
}else{
long max = Long.MIN_VALUE;
long min = Long.MAX_VALUE;
for(int i =0;i<a;i++){
long so = Math.abs(sc.nextLong());
if(i%2==0){
ans+=so;
min=Math.min(min,so);
}else{
ans-=so;
max= Math.max(max,so);
}
}
if(max>=min){
ans = ans+ max+max-min-min;
}
System.out.println(ans);
}
}
}
小蓝的礼物
import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int k = scan.nextInt();
int[] arr = new int[n];
for(int i=0;i<n;i++){
arr[i] = scan.nextInt();
}
Arrays.sort(arr);
int ans = 0;
for(int i=0;i<n;i++){
if(k > arr[i]){
k -= arr[i];
ans++;
}else{
if(k > (arr[i] + 1) / 2){
ans++;
}
break;
}
}
System.out.println(ans);
scan.close();
}
}
冒险者公会(贪心)
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static int f(int[]a, int max){
for (int i = 0; i < a.length; i++){
if(a[i] >= max){
int temp = a[i];
a[i] = -1;
return temp;
}
}
return -1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt(), n = sc.nextInt();
int[] a = new int[m];
for (int i = 0; i < m; i++) {
a[i] = sc.nextInt();
}
Arrays.sort(a);
int[][] b = new int[n][m];
for (int i = 0; i < n; i++) {
int k = sc.nextInt();
if (k > m) {
System.out.println(-1);
return;
}
for (int t = 0; t < k; t++) {
b[i][t] = sc.nextInt();
}
Arrays.sort(b[i]);
}
int[] max = new int[m];
int count = 0;
for (int i = 0; i < m; i++){
count = b[0][i];
for (int j = 0; j < n; j++){
count = Math.max(count, b[j][i]);
}
max[i] = count;
}
long sum = 0;
for (int i = 0; i < max.length; i++){
if (max[i] == 0){
continue;
}
int temp = f(a, max[i]);
if (temp != -1){
sum += temp;
}else {
System.out.println(-1);
return;
}
}
System.out.println(sum);
}
}
四个瓷瓶的神秘游戏
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] a = new int[4];
for (int i = 0; i < 4; i++){
a[i] = sc.nextInt();
}
Arrays.sort(a);
int count = a[0] + 2 * a[1];
a[3] = a[3] + 2 * a[0];
int cnt = a[1] - a[0];
a[3] += 3 * (cnt / 3);
cnt %= 3;
if (cnt == 2) a[3]++;
System.out.println(Math.max(count, a[3]));
}
}
双指针
简介
双指针算法是一种常用的算法技巧,它通常用于在数组或字符串中进行快速查找、匹配、排序或移动操作
双指针并非真的用指针实现,一般用两个变量来表示下标 (在后面都用指针来表示)
双指针算法使用两个指针在数据结构上进行迭代,并根据问题的要求移动这些指针
双指针往往也和单调性、排序联系在一起,在数组的区间问题上,暴力法的时间复杂度往往是O(n^2)的,但双指针利用“单调性”可以优化到O(n).
常见的双指针模型有:
- 对撞指针
- 快慢指针
对撞指针
指的是两个指针 left、right (简写为l,r) 分别指l向序列第一个元素和最后一个元素
然后l指针不断递增,r不断递减,直到两个指针的值相撞或错开 (即 l >= r),或者满足其他要求的特殊条件为止。
对撞指针一般用来解决有序数组或者字符串问题 (常见于区间问题)
查找有序数组中满足某些约束条件的一组元素问题: 比如二分查找、数字之和等问题字符串反转问题:反转字符串、回文数等问题.
- 使用两个指针 left,right。let 指向序列第一个元素,即: left = l,right 指向序列最后一个元素,即: right = n。
- 在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移,let ++。当满足另外定条件时,将右指针左移,right --
- 直到两指针相撞(即 left == right),或者满足其他要求的特殊条件时,跳出循环体
快慢指针
快慢指针一般比对撞指针更难想,也更难写
指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢
移动快的指针被称为快指针,移动慢的指针被称为慢指针
为了方便理解,我们成快指针为r,慢指针为1,这样慢指针和快指针构成区间[l,r]
两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止
- 使用两个指针1、r1一般指向序列第一个元素,即: 1= 1,r一般指向序列第零个元素,即:r = 0。即初始时区间[l, r]= [1,]表示为空区间
- 在循环体中将左右指针向右移动。当满足一定条件时,将慢指针右移,即1++。当满足另外一定条件时 (也可能不需要满足条件) ,将快指针右移,即r++,保持[l,r]为合法区间
- 到指针移动到数组尾端 (即l== n且r== n),或者两指针相交,或者满足其他特殊条件时跳出循环体
例题
聪明的小羊肖恩
import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int left = scan.nextInt();
int right = scan.nextInt();
int[] arr = new int[n];
for(int i=0;i<n;i++){
arr[i] = scan.nextInt();
}
Arrays.sort(arr);
System.out.println(calc(arr,right)-calc(arr,left-1));
scan.close();
}
static long calc(int[] arr,int t){
long ans = 0;
int l = 0 , r = arr.length - 1;
while(l < r){
while(l<r && arr[r] + arr[l] > t){
r--;
}
ans += r - l;
l++;
}
return ans;
}
}
神奇的数组
import java.util.*;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] arr = new int[n];
for(int i=0;i<n;i++){
arr[i] = scan.nextInt();
}
int l=0,r=0,rs=0;
long ans = 0;
while(l<n){
while(r<n && ((rs^arr[r]) == (rs + arr[r]))){
rs ^= arr[r];
r++;
}
ans += r - l;
rs ^= arr[l];
l++;
}
System.out.println(ans);
scan.close();
}
}
盛最多的水
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
class Solution {
public int maxArea(int[] height) {
int i = 0, j = height.length - 1, res = 0;
while(i < j) {
res = height[i] < height[j] ?
Math.max(res, (j - i) * height[i++]):
Math.max(res, (j - i) * height[j--]);
}
return res;
}
}
二分
概念
二分算法: 是用来在一个有序数组中查找某一元素的算法。时间复杂度O(log n)
在数学上的解释: 对于区间 [a , b] 上单调的函数y=f(x),通过不断地把函数 f() 的区间一分为二,使区间的两个端点逐步逼近,进而得到近似值的方法叫二分法。
两种模板
1.查找当前有序数组里面的大于等于x的第一个数
int[] arr = {1, 2, 3, 4, 5};
int l = 0, r = arr.length - 1;
int x = 2;
while (l < r){
int mid = ( l + r) / 2;
if (arr[mid] > x) r = mid - 1;
else l = mid + 1;
}
2.查找当前有序数组里面的大于等于x的最后一个数
int[] arr = {1, 2, 3, 4, 5};
int l = 0, r = arr.length - 1;
int x = 2;
while (l < r){
int mid = ( l + r + 1) / 2;
if (arr[mid] > x) r = mid - 1;
else l = mid;
}
例题
全部都有的子序列(利用了双指针)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] arr = new int[n];
Set<Integer> set = new HashSet<>();
for (int i = 0; i < n; i++) {
arr[i] = scan.nextInt();
set.add(arr[i]);//存储不重复的数字
}
int m = set.size();//理论最短长度
int l = 1, r = arr.length;
while (l < r) {
int mid = (l + r) / 2;//指的是滑动窗口的长度,而不是数组的位置
if (check(arr, mid, m)) {
r = mid;
} else {
l = mid + 1;
}
}
System.out.println(r);
scan.close();
}
public static boolean check(int[] arr, int mid, int m) {
//双指针
int n = arr.length;
int[] f = new int[1001];
int l = 0, r = 0, ans = 0;
while (r < n) {
f[arr[r]]++;
if (f[arr[r]] == 1) {
ans++;
}
if (r - l + 1 > mid) {//r - l + 1 代表滑动窗口的长度
f[arr[l]]--;
if (f[arr[l]] == 0) {
ans--;
}
l++;
}
r++;
if (ans >= m) {
return true;
}
}
return false;
}
}
可凑成的最大花束
import java.util.Scanner;
public class Main {
static int n, k;
static int[] arr;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
arr = new int[n];//保存每个数据
for(int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
long l = 0;
long r = (long)(2e14);
while(l < r) {
long mid = (l + r + 1) / 2; // 不是代表位置
if(check(mid)) l = mid; // mid变大
else r = mid - 1; // mid变小
}
System.out.println(l);
}
static boolean check(long x) {
long res = 0;
for(int i = 0; i < n; i++) {
res += Math.min(arr[i], x); // 计算有效花朵
}
return (res / x) >= k ;
}
}
妮妮的月饼工厂(和上面一样的思想)
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), k = sc.nextInt();
long[] arr = new long[n];
for (int i = 0; i < n; i++){
arr[i] = sc.nextInt();
}
long l = 0;
long r = (long) 1e10;
while (l < r){
long mid = (r + l +1) / 2; // 就是最后的高度
if (check(mid, arr, k)) l = mid;
else r = mid - 1;
}
System.out.println(l != 0 ? l : -1);
}
public static boolean check(long mid, long[] arr, long k){
long sum = 0;
for (int i = 0; i < arr.length; i++){
sum += arr[i] / mid;
}
return sum >= k;
}
}
体育健将(前缀和)
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), k = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++){
a[i] = sc.nextInt();
}
int[] b = new int[n];
int[] sum = new int[n];
for (int i = 0; i < n; i++){
b[i] = sc.nextInt();
sum[i] = b[i] + a[i];
}
Arrays.sort(sum);
long[] arr = new long[n + 1];
for (int i = 1; i < arr.length; i++){
arr[i] = arr[i - 1] + sum[i - 1];
}
int l = 0, r = arr.length - 1;
while (l < r){
int mid = (l + r) / 2;
if (arr[mid] >= k){
r = mid;
}else {
l = mid + 1;
}
}
if (arr[r] + a[r - 1] <= k) r++;
System.out.println(r);
}
}
倍增
概述
倍增算法是一种优化算法,通常应用于某些需要高效计算指数幂的场景。它基于分治的思想,通过反复求平方来实现快速计算指数幂的目的。在实际应用中,倍增算法经常用于解决最近公共祖先问题、二分查找等
快速幂
static long powMod(long b, long p, long m) {
long ans = 1;
while (p > 0) {
if ((p & 1) == 1) ans = ans * b % m;
b = b * b % m;
p >>>= 1;
}
return ans;
}
例题
import java.util.Scanner;
public class Main {
static int maxn = 110000;
static int n;
static int[] a = new int[maxn];
static int[][] f = new int[maxn][40];
static int query(int l, int r) {
//距离是r-l+1
int k = (int)(Math.log(r - l + 1) / Math.log(2));
return Math.min(f[l][k], f[r - (1 << k) + 1][k]);
}
// 从控制台读取一个整数n。
// 使用循环从控制台读取n个整数,并将它们存储在数组a中。
// 初始化一个二维数组f,其中f[i][j]表示从1到i之间第j小的数。
// 使用嵌套循环计算数组f的值,其中f[i][j]的值为f[i][j-1]和f[i+(1<<j-1)][j-1]中的较小值。
// 从控制台读取一个整数q。
// 使用循环执行q次以下操作:
// 从控制台读取两个整数l和r。
// 调用query函数,并将结果打印到控制台
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
for (int i = 1; i <= n; ++i) {
a[i] = sc.nextInt(); //账的多少
f[i][0] = a[i];
}
for (int j = 1; j <= (int)(Math.log(n) / Math.log(2)); j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
f[i][j] = Math.min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
int q = sc.nextInt();
while (q-- > 0) {
int l = sc.nextInt();
int r = sc.nextInt();
System.out.println(query(l, r));
}
}
}
例题
快速幂
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println(powMod(scan.nextLong(), scan.nextLong(), scan.nextLong()));
scan.close();
}
/**
* 计算 b 的 p 次幂对 m 取模的结果
* @param b 底数
* @param p 幂次
* @param m 取模值
* @return b 的 p 次幂对 m 取模的结果
*/
static long powMod(long b, long p, long m) {
long ans = 1;
while (p > 0) {
if ((p & 1) == 1) ans = ans * b % m;
b = b * b % m;
p >>>= 1;
}
return ans;
}
}
如今仍是遥远的理想之城1
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws IOException{
Scanner in = new Scanner(System.in);
int n = in.nextInt();
long k = in.nextLong();
int[] a = new int[200005];
for (int i = 1; i <= n; i++){
a[i] = in.nextInt();
}
int now = 1;
int cnt = 0;
int[] books = new int[200005];
while (books[now] == 0){
books[now] = cnt++;
now = a[now];
}
int temp = cnt;
cnt -= books[now];
if (k >= temp){
k -= temp;
k %= cnt;
}
k += temp;
now = 1;
while (k --> 0){
now = a[now];
}
System.out.println(now);
}
}
位运算
概述
例题
简单的异或难题
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt();
int[] a = new int[n + 1];
for (int i = 1; i <= n; i++){
a[i] = sc.nextInt();
}
int[] diff = new int[n + 1];
for (int i = 1; i <=n; i++){
diff[i] = diff[i - 1] ^ a[i];
}
while (m --> 0){
int l = sc.nextInt(), r = sc.nextInt();
System.out.println(diff[r] ^ diff[l - 1]);
}
sc.close();
}
}
出列
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
// 001
// 010
// 011
// 第一次移除的都是结尾为1 第二次结尾为10 第三次结尾为100
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int v = 0;
while((1 << v) <= n) {
v++;
}
System.out.println(1 << (v - 1));
}
}
小蓝学位运算
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
static long mod = (long)1e9+7;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n];
long[] xor = new long[n + 1];
for (int i = 0;i < n; i++){
a[i] = sc.nextInt();
xor[i + 1] = xor[i] ^ a[i] % mod;
}
if (n >= 8192){
System.out.println("0");
return;
}
long sum = 1;
for (int i = 1; i <= n; i++){
for (int j = i; j <= n; j++){
sum = (sum * (xor[i - 1] ^ xor[j])) & mod;
}
}
System.out.println(sum);
}
}
位移
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException{
input in = new input();
int t = in.nextInt();
while (t --> 0){
int a = in.nextInt();
int b = in.nextInt();
String s1 = Integer.toString(a, 2);
String s2 = Integer.toString(b, 2);
int end = -1;
for (int i = s2.length() - 1; i >= 0; i--){
if (s2.charAt(i) != '0'){
end = i;
break;
}
}
s2 = s2.substring(0, end + 1);
if (s1.contains(s2)) System.out.println("Yes");
else System.out.println("No");
}
}
}
class input{
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public int nextInt() throws IOException{
in.nextToken();
return (int)in.nval;
}
}
选题
重点:遍历所有的子集
例如,假设我们有一个长度为3的数组a = [1, 2, 3]。
当i = 0(二进制:000)时,子集为空集,不满足条件。
当i = 1(二进制:001)时,选中的元素是a[0],子集为{1},可能不满足至少包含3个元素的条件。
当i = 2(二进制:010)时,选中的元素是a[1],子集为{2},同样可能不满足至少包含3个元素的条件。
当i = 3(二进制:011)时,选中的元素是a[0]和a[1],子集为{1, 2},此时需要检查这个子集是否满足给定的条件(和值范围、差值以及至少包含3个元素)。
当i = 4(二进制:100)时,选中的元素是a[2],子集为{3},同样可能不满足至少包含3个元素的条件。
当i = 5(二进制:101)时,选中的元素是a[0]和a[2],子集为{1, 3},再次检查子集是否满足所有条件。
当i = 6(二进制:110)时,选中的元素是a[1]和a[2],子集为{2, 3},检查子集是否满足条件。
当i = 7(二进制:111)时,选中了所有元素,子集为{1, 2, 3},此时会检查这个子集是否满足条件。
循环遍历直到2^3=8次,分别对应所有可能的子集组合,并对每个子集进行条件判断。
这里(i >> j)表示将整数i右移j位,& 1则是对结果进行与1的按位与操作。
当j=0时,(i >> 0) & 1实际上就是检查i的最低位是否为1,如果是,则代表a[0]这个元素被选中。
当j=1时,(i >> 1) & 1检查的是i的次低位(原索引为1的位置)是否为1,以此类推。
假设n=3,i=5(二进制形式为101),当:
j=0时,(5 >> 0) & 1的结果是1,所以a[0]被选中;
j=1时,(5 >> 1) & 1的结果也是1,所以a[1]被选中;
j=2时,(5 >> 2) & 1的结果是0,所以a[2]未被选中。
因此,在此例中,子集包含了数组中的a[0]和a[1]两个元素。通过遍历所有可能的i值,就可以得到数组的所有可能子集。
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
// 读取输入的数组长度
int l = sc.nextInt();
// 读取最小和
int r = sc.nextInt();
// 读取最大和
int x = sc.nextInt();
// 读取差值
int[] a = new int[n];
// 创建长度为n的整型数组
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
// 读取n个整数并存入数组
}
int ans = 0;
// 初始化结果为0
for (int i = 0; i < (int) Math.pow(2, n); i++) {
int sum = 0;
// 初始化和为0
Set<Integer> set = new HashSet<>();
// 使用HashSet存储数组元素
int min = Integer.MAX_VALUE;
// 初始化最小值为最大整数
int max = Integer.MIN_VALUE;
// 初始化最大值为最小整数
for (int j = 0; j < n; j++) {
if (((i >> j) & 1) == 1) {
set.add(a[j]);
// 将数组元素加入HashSet
sum += a[j];
// 累加和
min = Math.min(min, a[j]);
// 更新最小值
max = Math.max(max, a[j]);
// 更新最大值
}
}
if (sum >= l && sum <= r && max - min >= x && set.size() >= 3) {
ans++; // 满足条件则结果加1
}
}
// 输出结果
System.out.println(ans);
}
}
第三部分搜索
搜索
DFS基础
概念
DFS(深度优先搜索)是基于递归求解问题。而针对搜索的过程
我们约定,对于问题的介入状态,叫初始状态,要求的状态叫目标状态
这里的搜索就是对实时产生的状态进行分析检测,直到得到一个目标状态或符合要求的最佳状态为止。对于实时产生新的状态的过程叫扩展
搜索的要点:
- 选定初始状态,在某些问题中可能是从多个合法状态分别入手搜索
- 遍历自初始状态或当前状态所产生的合法状态,产生新的状态并进入递归;
- 检查新状态是否为目标状态,是则返回,否则继续遍历,重复2-3步骤
模板
public static void dfs(){
if (当前状态 == 目标状态){
//逻辑处理
return;
}
for (寻找状态){
if (状态合法){
dfs(新状态);
}
}
}
DFS回溯
概念
回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试,从而搜索到抵达特定终点的一条或者多条特定路径。
回溯在于“状态”。在回溯算法向前的每一步,你都会去设置某个状态,而当向前走走不通的时候回退,此时需要把之前设置的状态撤销掉。
比如给定数组[1,2,3],求所有可能的全排列。
如果让我们在纸上写的话,很容易可以写出来[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
模板
public static void dfs(){
if (当前状态 == 目标状态){
//逻辑处理
return;
}
for (查找新状态){
if (状态合法){
//标记新状态
dfs(新状态);
//撤销标记
}
}
}
例题:开心
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
static Long max = Long.MIN_VALUE, min = Long.MAX_VALUE;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
char[] c = sc.next().toCharArray();
int k = sc.nextInt();
StringBuilder str = new StringBuilder();
dfs(0, c, k, str);
System.out.println(max - min);
sc.close();
}
/**
* 深度优先搜索函数
* @param u 当前节点
* @param c 字符数组
* @param k 剩余的数字个数
* @param str 当前生成的字符串
*/
public static void dfs(int u, char[] c, int k, StringBuilder str) {
if (u == c.length) {
if (k == 0) {
long res = 0;
String[] s = str.toString().split("\\+");
for (String string : s) {
res += Long.parseLong(string);
}
max = Math.max(max, res);
min = Math.min(min, res);
}
return;
}
str.append(c[u]);
dfs(u + 1, c, k, str);
str.deleteCharAt(str.length() - 1);
if (k > 0 && u < c.length - 1){
str.append(c[u]);
str.append("+");
dfs(u + 1, c, k - 1, str);
str.deleteCharAt(str.length() - 1);
str.deleteCharAt(str.length() - 1);
}
}
}
DFS-剪枝
概念
在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些“枝条”,故称前枝。应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。
概况:在进行搜索算法的过程中,将已知无意义的情况排除的行为叫做剪枝
例题:特殊的三角形
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long[] sum = new long[(int)1e6 + 1];
for (int a = 1; a < 100; a++){
for (int b = a + 1; b < 1000; b++){
for (int c = b + 1; c < a + b; c++){
if (is(a, b, c) && a * b * c < 1e6 + 1){
sum[a * b * c]++;
}
}
}
}
for (int i = 1; i < sum.length; i++) {
sum[i] += sum[i-1];
}
int t = sc.nextInt();
while (t-->0){
int l = sc.nextInt();
int r = sc.nextInt();
System.out.println(sum[r]-sum[l - 1]);
}
}
public static boolean is(int a, int b, int c){
return (a + b > c) && (a + c > b) && (b + c > a);
}
}
DFS-例题
1.仙境诅咒
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
static int D;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] x = new int[n];
int[] y = new int[n];
int[] res = new int[n];
for (int i = 0; i < n; i++){
x[i] = sc.nextInt();
y[i] = sc.nextInt();
}
D = sc.nextInt();
res[0] = 1;
dfs(x, y, res,0);
for (int k : res) System.out.println(k);
}
public static void dfs(int[] x, int[] y, int[] res, int start) {
int n = res.length;
int x0 = x[start], y0 = y[start];
for (int i = 0; i < n; i++){
if (res[i] == 1 || start == i) continue;
int x1 = x[i], y1 = y[i];
int dis = (x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1);
if (dis <= D * D){
res[i] = 1;
dfs(x, y, res, i);
}
}
}
}
2.黄金树
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
static int n;
//左儿子
static int[] z;
//右儿子
static int[] y;
static int count = 0;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
int[] w = new int[n + 1];
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();
}
z = new int[n + 1];
y = new int[n + 1];
//黄金指数
int[] ans = new int[n + 1];
for (int i = 1; i <= n; i++){
int l = sc.nextInt();
z[i] = l;
int r = sc.nextInt();
y[i] = r;
if (l != -1) {
ans[l] = ans[i] + 1;
}
if (r != -1) {
ans[r] = ans[i] - 1;
}
}
dfs(1, w, ans);
System.out.println(count);
sc.close();
}
public static void dfs(int i, int[] w, int[] ans){
if (ans[i] == 0) {
count += w[i];
}
for (int j = 1; j <= 2; j++){
if (j == 1 && z[i] != -1) {
dfs(z[i], w, ans);
}
if (j == 2 && y[i] != -1) {
dfs(y[i], w, ans);
}
}
}
}
3.混境之地2
import java.util.Scanner;
public class Main {
static int N = 1010;
static int n, m;
static int A, B, C, D;
static char[][] arr = new char[N][N];
static int[][] color = new int[N][N];
//右左下上
static int[] dx = { 1, -1, 0, 0 };
static int[] dy = { 0, 0, -1, 1 };
static void dfs(int x, int y, int c) {
color[x][y] = c;
for (int i = 0; i < 4; i++) {
int xx = x + dx[i];
int yy = y + dy[i];
// 剪枝
if (xx < 0 || xx >= n || yy < 0 || yy >= m || color[xx][yy] != 0 || arr[xx][yy] == '#') {
continue;
}
dfs(xx, yy, c);
}
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
m = scan.nextInt();
A = scan.nextInt() - 1;
B = scan.nextInt() - 1;
C = scan.nextInt() - 1;
D = scan.nextInt() - 1;
for (int i = 0; i < n; ++i) {
arr[i] = scan.next().toCharArray();
}
// 类比成染色
dfs(A, B, 1);
if (color[C][D] == 1) {
System.out.println("Yes");
return;
}
// 将后半部分染色
dfs(C, D, 2);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (arr[i][j] == '#' && check(i, j)) {
System.out.println("Yes");
return;
}
}
}
System.out.println("No");
}
static boolean check(int x, int y) {
int u = 0;
for (int i = 0; i < 4; ++i) {
int tx = x + dx[i], ty = y + dy[i];
if (tx < 0 || ty < 0 || tx >= n || ty >= m) {
continue;
}
u |= color[tx][ty];
}
return u == 3;
}
}
记忆化搜索
第四部分动态规划
动态规划基础
线性DP
动态规划概念
动态规划简称 DP,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决然后呢,把子问题答案保存起来,以减少重复计算,再根据子问题答案反推,得出原问题解的一种方法。
动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算
动态规划的几个步骤
- 即划分子问题
- 状态表示。一般用数dp[ ]表示当前状态
- 状态转移,即当前状态是由前面那些状态转移过来的 例如 dp=dpi-11表示当前状态可以由上一个状态转移过来
- 确定边界,确定初始状态
概念
线性DP是动态规划问题中的一类问题,指状态之间有线性关系的动态规划问题
所谓线性DP,就是递推方程是有一个明显的线性关系的,一维线性和二维线性都有可能。
而我们在求的时候,有一个明显的求的顺序: 即一行一行地来求。这样的有线性顺序的叫做线性DP
例题
拍照
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] arr = new int[1005];
int[][] dp = new int[2][1005];
for (int i = 1; i <= n; i++){
arr[i] = sc.nextInt();
}
for (int i = 1; i < n; i++){
for (int j = i + 1; j <= n; j++) {
if (arr[j] >= arr[i]) {
dp[0][j] = Math.max(dp[0][i] + 1, dp[0][j]);
}
}
}
for (int i = n; i > 1; i--){
for (int j = i - 1; j >= 1; j--) {
if (arr[j] >= arr[i]) {
dp[1][j] = Math.max(dp[1][i] + 1, dp[1][j]);
}
}
}
int min = Integer.MAX_VALUE;
for (int i = 1; i <= n; i++){
min = Math.min(min,n - dp[0][i] - dp[1][i] - 1);
}
System.out.println(min);
}
}
可构造的序列总数
长度为 i ,小于 j 的总数
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int k = sc.nextInt();
int n = sc.nextInt();
int mod = (int)1e9 + 7;
//长度为n,大小为k
int[][] dp = new int[n + 1][k + 1];
for (int i = 1; i <= k; i++){
dp[1][i] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
for (int l = j; l <= k; l += j){
dp[i + 1][l] += dp[i][j];
dp[i + 1][l] %= mod;
}
}
}
int ans = 0;
for (int i = 1; i <= k; i++){
ans += dp[n][i];
ans %= mod;
}
System.out.println(ans);
}
}
最快洗车时间
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] t = new int[n];
int sum = 0;
for (int i = 0; i < n; i++) {
t[i] = sc.nextInt();
sum += t[i];
}
int[] dp = new int[sum + 1];
int temp = sum / 2;
for (int i = 0; i < n; i++) {
for (int j = sum; j >= t[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - t[i]] + t[i]);
}
}
int ans = Integer.MAX_VALUE;
for (int i = 1; i <= sum; i++) {
ans = Math.min(ans, Math.max(sum - dp[i],dp[i]));
}
System.out.println(ans);
}
}
安全序列
思路:
令 dp[i] 表示在工厂直线的 i 个空位上,按照题目要求放置油桶的方法数。
当我们在第 i 个位置放置一个油桶时,这个油桶不能紧挨着前一个油桶(至少需要 n 个空位隔开),因此前一个油桶只能放在 i - k - 1 位置。所以,从 dp[i - k - 1] 移动到 dp[i] 的方案数就是 dp[i - k - 1]。
如果我们在第 i 个位置不放置油桶,则当前的状态与前一状态相同,即有 dp[i - 1] 种放置方法。
因此,将这两种情况相加即可得到在 i 个空位上的所有合法放置油桶的方法数,即 dp[i] = dp[i - 1] + dp[i - k -1]
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
static Scanner sc = new Scanner(System.in);
static int n = sc.nextInt();
static int k = 2;
static int mod = (int) 1e9 + 7;
static int[] dp = new int[n + 1];
public static void main(String[] args) {
dp[0] = 1;
for (int i = 1; i <= n; i++){
if (i - k > 1) {
dp[i] = (dp[i - 1] + dp[i - k - 1]) % mod;
} else {
dp[i] = (dp[i - 1] + 1) % mod;
}
}
System.out.println(dp[n]);
}
}
二维DP
概念
当状态都只有一个维度,一般为dp[ ],称之为线性动态规划
当维度有两个的时候,需要用二维的状态来解决问题,例如棋盘、矩阵、路径等类别的问题
更准确的说,二维动态规划指线性动态规划的拓展,在一个平面上做动态规划
例题
地图
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
static int N = 101;
static int n, m, k;
static char[][] s = new char[N][N];
static int[][][][] f = new int[N][N][2][6];
static int[] dx = {0, 1};
static int[] dy = {1, 0};
public static int dfs(int x, int y, int d, int step) {
if(x >= n || y >= m) {
return 0;
}
if (s[x][y] == '#') {
return 0;
}
if (step > k) {
return 0;
}
if (x == n - 1 && y == m - 1) {
return 1;
}
if (f[x][y][d][step] != 0) {
return f[x][y][d][step];
}
int res = 0;
for (int i = 0; i < 2; i++) {
res += dfs(x + dx[i], y + dy[i], i, step + (i != d ? 1 : 0));
}
return f[x][y][d][step] = res;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
k = sc.nextInt();
sc.nextLine();
for (int i = 0; i < n; i++) {
s[i] = sc.nextLine().toCharArray();
}
int ans = 0;
if (s[0][1] != '#') {
ans += dfs(0, 1, 0, 0);
}
if (s[1][0] != '#') {
ans += dfs(1, 0, 1, 0);
}
System.out.println(ans);
}
}
电影放映计划
思路:
import java.util.*;
/**
* @author ASUS
*/
public class Main {
static final int N = (int)1e6 + 10;
static int[] f = new int[N];
//时间
static int[] v = new int[N];
//利润
static int[] w = new int[N];
static int n,m,k;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
m = sc.nextInt();
n = sc.nextInt();
for(int i = 1; i <= n; i ++) {
v[i] = sc.nextInt();
w[i] = sc.nextInt();
}
k = sc.nextInt();
for(int i = 1; i <= n; i ++) {
v[i] += k;
}
m += k;
for(int i = 1; i <= n;i++) {
for(int j = v[i]; j <= m; j ++) {
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
}
System.out.println(f[m]);
}
}
LIS(最长上升子序列)
概念(以 i 为结尾的最长子序列,也就是最后一个是索引 i 代表的)
例题
蓝桥勇士
import java.util.*;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] f = new int[n];
for (int i = 0; i < n; i++) {
f[i] = sc.nextInt();
}
int[] dp = new int[n];
dp[0] = 1;
int max = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < n; j++){
if (f[j] < f[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
} else {
dp[i] = Math.max(dp[i], 1);
}
max = Math.max(dp[i], max);
}
}
System.out.println(max);
}
}
LCS(最长公共子序列)
概念
例题(最长公共子序列)
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[] a = new int[n];
int[] b = new int[m];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
for (int i = 0; i < m; i++) {
b[i] = sc.nextInt();
}
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i - 1] == b[j - 1]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
} else {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
dp[i][j] = Math.max(dp[i][j], dp[i][j - 1]);
}
}
}
System.out.println(dp[n][m]);
}
}
背包问题
01背包
模型
有一个体积为V的背包,商店有n个物品,每个物品有一个价值v和体积w,每个物品只能被拿一次,问能够装下物品的最大价值。
这里每一种物品只有两种状态即“拿”或“不拿”
设状态dp[i][j]表示到第i个物品为止,拿的物品总体积为i的情况下的最大价值。
我们并不关心某个物品有没有被拿,只关心当前体积下的最大价值
转移方程为:dp[i][j]= max(dp[i-1][j],dp[i-1][j-w]+v);如果不拿物品i,那么最大价值就是dp[i-1][j],如果拿了就是从体积j-v转移过来,体积会变大w,价值增加v。
最后输出dp[n][Vl;
例题
小明的背包1
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int V = sc.nextInt();
int[] w = new int[N];
int[] v = new int[N];
int[][] dp = new int[N + 1][V + 1];
for (int i = 0; i < N; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= V; j++) {
if (j >= w[i - 1]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
}
}
System.out.println(dp[N][V]);
}
}
倒水
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt();
long[] a = new long[n + 1];
long[] b = new long[n + 1];
long[] c = new long[n + 1];
long[] d = new long[n + 1];
long[] e = new long[n + 1];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextLong();
b[i] = sc.nextLong();
c[i] = sc.nextLong();
d[i] = sc.nextLong();
e[i] = sc.nextLong();
}
long[][] dp = new long[n + 1][m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = dp[i - 1][j] + e[i];
if (j >= a[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][(int)(j - a[i])] + b[i]);
}
if (j >= c[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][(int)(j - c[i])] + d[i]);
}
}
}
System.out.println(dp[n][m]);
}
}
盗墓分赃
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n + 1];
int sum = 0;
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
sum += a[i];
}
if (sum % 2 != 0) {
System.out.println("no");
return;
}
sum /= 2;
int[][] dp = new int[n + 1][sum + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= sum; j++) {
if (j >= a[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - a[i]] + a[i]);
}
}
if (dp[i][sum] == sum) {
System.out.println("yes");
return;
}
}
System.out.println("no");
}
}
小蓝的神秘礼物
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int v = sc.nextInt();
int n = sc.nextInt();
int[] a = new int[n + 1];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
int[][] dp = new int[n + 1][v + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= v; j++) {
if (j >= a[i]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - a[i]] + a[i]);
}
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j]);
}
}
System.out.println(v - dp[n][v]);
}
}
完全背包
概念
完全背包也叫无穷背包,即每个物品都有无数个的背包
有一个体积为v的背包,商店有口个物品,每个物品有一个价值v和体积w,每个物品有无限多
个,可以被拿无穷次,问能够装下物品的最大价值。
这里每一种物品只有无穷种状态即"拿0个,1个,2个,无穷多个".
设状态dp[ ]表示拿的物品总体积为的情况下的最大价值。
我们并不关心某个物品拿了几个,只关心当前体积下的最大价值。
转移方程为:dp[i] = max(dp[i-w]+v, dp[i]);
dp[i][j] = max(dp[i - 1][v - w[i]] + c[i], dp[i - 1][v]);
现在就必须使用 "新数据"来更新"新数据",因
为新数据中包括了拿当前这个物品的状态,而当前这个物品是可以被拿无数次的。
最后输出dp[v]即可。
例题
小明的背包2
import java.util.Scanner;
/**
* @author ASUS
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt();
int[] w = new int[n + 1];
int[] v = new int[n + 1];
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
}
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (j >= w[i]) {
dp[i][j] = Math.max(dp[i][j - w[i]] + v[i], dp[i - 1][j]);
}else {
dp[i][j] = dp[i - 1][j];
}
}
}
System.out.println(dp[n][m]);
}
}
多重背包
概念
例题(小明的背包3)
import java.util.Scanner;
public class Main {
public static void main(String[] arge) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[] w = new int[n];
int[] v = new int[n];
int[] s = new int[n];
for(int i = 0; i < n; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
s[i] = sc.nextInt();
}
int[][] dp = new int[n + 1][m + 1];
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
for(int k = 0; k <= Math.min(s[i - 1], j / w[i - 1]); k++) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * w[i - 1]] + k * v[i - 1]);
}
}
}
System.out.println(dp[n][m]);
sc.close();
}
}
二进制优化
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] arge) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[] w = new int[n];
int[] v = new int[n];
int[] s = new int[n];
ArrayList<Integer> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
for(int i = 0; i < n; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
s[i] = sc.nextInt();
for(int k = 1; k <= s[i]; k *= 2) {
a.add(v[i] * k);
b.add(w[i] * k);
s[i] -= k;
}
if(s[i] > 0) {
a.add(s[i] * v[i]);
b.add(s[i] * w[i]);
}
}
int[] dp = new int[m + 1];
for(int i = 0; i < a.size(); i++) {
int v1 = a.get(i);
int w1 = b.get(i);
for(int j = m; j >= w1; j--) {
dp[j] = Math.max(dp[j], dp[j - w1] + v1);
}
}
System.out.println(dp[m]);
sc.close();
}
}
二维费用背包和分组背包
二维度费用背包
分组背包
例题
小明的神秘行囊
import java.util.Scanner;
public class Main {
public static void main(String[] arge) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int v = sc.nextInt();
int m = sc.nextInt();
int[] V = new int[n];
int[] M = new int[n];
int[] W = new int[n];
for(int i = 0; i < n; i++) {
V[i] = sc.nextInt();
M[i] = sc.nextInt();
W[i] = sc.nextInt();
}
int[][] dp = new int[v + 1][m + 1];
for (int i = 1; i <= n; i++) {
for (int j = v; j >= V[i - 1]; j--) {
for (int k = m; k >= M[i - 1]; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - V[i - 1]][k - M[i - 1]] + W[i - 1]);
}
}
}
System.out.println(dp[v][m]);
sc.close();
}
}
小明的背包5
import java.util.Scanner;
public class Main {
public static void main(String[] arge) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int v = sc.nextInt();
int[] s = new int[n];
int[][] V = new int[101][101];
int[][] W = new int[101][101];
for (int i = 0; i < n; i++) {
s[i] = sc.nextInt();
for (int j = 0; j < s[i]; j++) {
W[i][j] = sc.nextInt();
V[i][j] = sc.nextInt();
}
}
int[][] dp = new int[n + 1][v + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= v; j++) {
dp[i][j] = dp[i - 1][j];
for (int k = 0; k < s[i - 1]; k++) {
if (j >= W[i - 1][k]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - W[i - 1][k]] + V[i - 1][k]);
}
}
}
}
System.out.println(dp[n][v]);
sc.close();
}
}
树形DP
自上而下树形DP
import java.util.Scanner;
/**
* 主程序类,包含主要的逻辑和入口方法。
*/
public class Main {
// 定义最大节点数和最大边数
static final int maxn = 110000;
static int n; // 节点数
static int[] val = new int[maxn]; // 存储节点的值
static Edge[] edges = new Edge[maxn << 1]; // 存储边
static int[] head = new int[maxn]; // 存储每个节点的头指针
static int cnt; // 边的计数器
static int[][] f = new int[maxn][2]; // 存储动态规划的结果
/**
* 边的类,用于表示图中的边。
*/
static class Edge {
int next, to;
Edge(int next, int to) {
this.next = next;
this.to = to;
}
}
/**
* 添加一条边到图中。
* @param from 起始节点
* @param to 终止节点
*/
public static void add(int from, int to) {
edges[++cnt] = new Edge(head[from], to);
head[from] = cnt;
}
/**
* 深度优先搜索,计算每个节点的最大值和次最大值。
* @param u 当前节点
* @param fa 父节点
*/
public static void dfs(int u, int fa) {
for (int i = head[u]; i != 0; i = edges[i].next) {
int v = edges[i].to;
if (v == fa)
continue;
dfs(v, u);
// 更新当前节点的最大值和次最大值
f[u][0] += Math.max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
}
/**
* 主入口方法,读取输入,构建图,并计算结果。
* @param args 命令行参数
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
// 初始化节点值和次最大值
for (int i = 1; i <= n; ++i) {
val[i] = sc.nextInt();
f[i][1] = val[i];
}
// 构建图
for (int i = 1; i < n; ++i) {
int u = sc.nextInt();
int v = sc.nextInt();
add(u, v);
add(v, u);
}
// 计算结果
dfs(1, 0);
// 输出最大值
System.out.println(Math.max(f[1][0], f[1][1]));
sc.close();
}
}
import java.util.Scanner;
import java.util.Arrays;
public class Main {
// 定义最大节点数和背包容量
static final int maxn = 110;
static int n, V;
// 动态规划数组和权重、体积数组
static int[][] f = new int[maxn][maxn];
static int[] w = new int[maxn], val = new int[maxn];
// 边的数组
static Edge[] edges = new Edge[maxn << 1];
static int[] head = new int[maxn];
static int cnt; // 边的计数器
// 边的定义
static class Edge {
int next, to;
Edge(int next, int to) {
this.next = next;
this.to = to;
}
}
/**
* 添加一条边到图中
*
* @param from 起点
* @param to 终点
*/
public static void add(int from, int to) {
edges[++cnt] = new Edge(head[from], to);
head[from] = cnt;
}
/**
* 深度优先搜索,进行动态规划
*
* @param u 当前节点
* @param fa 父节点
*/
public static void dfs(int u, int fa) {
// 初始化动态规划数组
Arrays.fill(f[u], Integer.MIN_VALUE);
// 如果当前节点的体积不超过背包容量,更新对应位置的值
if (val[u] <= V) {
f[u][val[u]] = w[u];
}
// 遍历当前节点的所有邻居节点
for (int i = head[u]; i != 0; i = edges[i].next) {
int v = edges[i].to;
// 跳过父节点
if (v == fa)
continue;
// 递归搜索邻居节点
dfs(v, u);
// 通过状态转移更新当前节点的动态规划数组
int[] nf = Arrays.copyOf(f[u], V + 1);
for (int v1 = 0; v1 <= V; v1++) {
for (int v2 = 0; v1 + v2 <= V; v2++) {
nf[v1 + v2] = Math.max(nf[v1 + v2], f[u][v1] + f[v][v2]);
}
}
// 更新当前节点的动态规划数组
for (v = 0; v <= V; v++)
f[u][v] = nf[v];
}
}
/**
* 主函数,读取输入并执行动态规划
*
* @param args 输入参数
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
V = sc.nextInt();
// 初始化节点的权重和体积
for (int i = 1; i <= n; ++i) {
w[i] = sc.nextInt();
val[i] = sc.nextInt();
}
/*
构建图的边
这段代码通过读取输入,将每对节点之间建立双向边,从而构建图。
输入格式假定为每行两个整数u和v,表示一条边的两个端点。
这个过程会遍历n-1次,确保了图的连通性。
*/
for (int i = 1; i < n; ++i) { // 从第2个节点开始,遍历至第n个节点
int u = sc.nextInt(); // 读取边的起始节点
int v = sc.nextInt(); // 读取边的结束节点
add(u, v); // 添加边(u, v),这里假设add方法为添加边的函数
add(v, u); // 添加边(v, u),实现双向边的连接
}
// 动态规划求解
dfs(1, 0);
// 找到最大价值
int ans = 0;
for (int i = 0; i <= V; ++i)
ans = Math.max(ans, f[1][i]);
// 输出最大价值
System.out.println(ans);
sc.close();
}
}
自下而上树形DP
最大独立集
import java.util.*;
/**
* 主程序类,解决特定问题。
* @author AUSU
*/
public class Main {
// 定义常量N,表示节点的最大数量
static final int N = 100005;
// 数组a存储节点的权重
static int[] a = new int[N];
// dp数组用于动态规划计算,dp[u][0]表示不包含节点u的最大值,dp[u][1]表示包含节点u的最大值
static long[][] dp = new long[N][2];
// 邻接表e表示节点之间的关系
static List<Integer>[] e = new ArrayList[N];
// 变量n表示节点的数量
static int n;
/**
* 深度优先搜索函数,用于递归计算每个节点的最大值。
* @param u 当前遍历的节点
*/
public static void dfs(int u) {
// 遍历当前节点的所有子节点
for (int v : e[u]) {
dfs(v); // 递归遍历子节点
dp[u][1] += dp[v][0]; // 计算包含当前节点的最大值
dp[u][0] += Math.max(dp[v][0], dp[v][1]); // 计算不包含当前节点的最大值
}
dp[u][1] += a[u]; // 加上当前节点的权重
}
/**
* 主函数,程序的入口点。
* @param args 命令行参数
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建输入扫描器
n = scanner.nextInt(); // 读取节点数量
// 初始化节点权重和邻接表
for (int i = 1; i <= n; i++) {
a[i] = scanner.nextInt();
e[i] = new ArrayList<>();
}
// 构建邻接表,表示节点之间的关系
for (int i = 1; i < n; i++) {
int x = scanner.nextInt();
int y = scanner.nextInt();
e[y].add(x);
}
dfs(1); // 从根节点开始深度优先搜索
// 输出最大值
System.out.println(Math.max(dp[1][0], dp[1][1]));
}
}
最小点覆盖
import java.util.*;
public class Main {
// 定义最大节点数
static final int N = 100005;
// 邻接表,存储图的结构
static List<Integer>[] e = new ArrayList[N];
// 动态规划数组,dp[i][0]表示以i为根节点时,不包含i的最大权值,dp[i][1]表示以i为根节点时,包含i的最大权值
static int[][] dp = new int[N][2];
// 图的节点数
static int n;
/**
* 深度优先搜索遍历图,计算每个节点的最大权值。
* @param u 当前遍历的节点
* @param fa 父节点
*/
public static void dfs(int u, int fa) {
// 遍历当前节点的所有子节点
for (int v : e[u]) {
// 跳过父节点
if (v == fa) continue;
// 递归遍历子节点
dfs(v, u);
// 更新不包含当前节点的最大权值
dp[u][0] += dp[v][1];
// 更新包含当前节点的最大权值
dp[u][1] += Math.min(dp[v][0], dp[v][1]);
}
// 包含当前节点的最大权值增加1
dp[u][1] += 1;
}
/**
* 程序入口。
* @param args 命令行参数
*/
public static void main(String[] args) {
// 读取输入
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
// 初始化邻接表
for (int i = 1; i <= n; i++) {
e[i] = new ArrayList<>();
}
// 构建图的邻接表表示
for (int i = 1; i < n; i++) {
int x = scanner.nextInt();
int y = scanner.nextInt();
e[x].add(y);
e[y].add(x);
}
// 使用深度优先搜索计算每个节点的最大权值
dfs(1, 0);
// 输出结果
System.out.println(Math.min(dp[1][0], dp[1][1]));
}
}