本文为我的第13届蓝桥杯前几天突击,突击失败,JAVA A组今年太难了
排序
1.归并排序
时间复杂度 : n * log n 空间 n
思想:对数组分治,一半一半的分,直到分至一个元素则顺序已不用动,然后左右两边合并,用一个辅助数组,从小到大将左右两边按顺序排好,再将其复制到原数组的原位置(这样部分就排好序了),依次层层回溯,最终数组便排好。
class Solution {
int[] temp;
public void mergeSort(int[] nums, int l, int r) {
if(l >= r)
return;
int mid = r + ((l - r) >> 1);//取中分两半
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
int k = l, i = l, j = mid + 1;//将l~r区域排好序
while(i <= mid && j <= r)//一边放完就结束
temp[k++] = nums[i] <= nums[j]? nums[i++]:nums[j++];
//哪边还没放完继续放
while(i <= mid)
temp[k++] = nums[i++];
while(j <= r)
temp[k++] = nums[j++];
for(int t = l; t <= r; t++)//更新nums的l~r顺序
nums[t] = temp[t];
}
public int[] sortArray(int[] nums) {
int len = nums.length;
temp = new int[len];
mergeSort(nums, 0, len - 1);
return nums;
}
}
2.快速排序
时间复杂度同上,空间1,不过快速排序有更高的效率,时间常数较小,但是当基准值选取不好,原来的序列基本就是排好序的,就会退化为冒泡排序,时间复杂度n * n,所以我加了个三值取中法优化基准值的选取,如上图效果的差距很大
思想:分治法,每次选取一个基准值,将其定在区间最左,左右双指针定在两端,然后,用一个temp记录这个基准值,这里就相当于变成了一个坑,等待数值的填充,右侧指针左移直到遇到比基准值小或等的数字,将其填在左指针处,然后,右指针相当于一个坑,左指针右移,直到遇到大于基准的数停下,将其填充在右指针处,就这样一直循环,直到两指针相遇,此处放基准值,那么左边全是比基准小的,右侧全是大的,再分别对基准左右两部分区间(不包含基准)进行同样的操作,最终就排好了序
class Solution {
//三数取中优化,防止极端情况退化为冒泡排序
void selectPivot(int[] nums, int l, int r) {
//如果不到3个数,不用再三数取中了
if (l >= r - 1)
return;
int mid = l + ((r - l) >> 1);
int pivot = nums[l] <= nums[r]? l : r;//选出两端较小点
if(nums[mid] > nums[l - pivot + r])//刚才比较大的点 最大
pivot = l - pivot + r;
else if(nums[mid] > nums[pivot])//mid为中间值
pivot = mid;
//pivot恰为中间值,不变
if(pivot != l){
//将中值交换至左端
nums[l] ^= nums[pivot];
nums[pivot] ^= nums[l];
nums[l] ^= nums[pivot];
}
}
void quickSort(int[] nums, int l, int r) {
if(l >= r)
return;
selectPivot(nums, l, r);
int temp = nums[l], left = l, right = r;
while(left < right){
while(right > left && nums[right] > temp)
right--;
nums[left] = nums[right];
while(right > left && nums[left] <= temp)
left++;
nums[right] = nums[left];
}
nums[left] = temp;
quickSort(nums, l, left - 1);
quickSort(nums, left + 1, r);
}
public int[] sortArray(int[] nums) {
int len = nums.length;
quickSort(nums, 0, len - 1);
return nums;
}
}
字符串
KMP
acwing模板:
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
459. 重复的子字符串
class Solution {
public static boolean repeatedSubstringPattern(String s) {
int len = s.length(), cnt = 0;
int[] ne = new int[len + 1];
char[] p = s.toCharArray();
//第i个字符,1的·next必为0,直接从第二个开始
for (int i = 2, j = 0; i <= len; i++) {
while (j > 0 && p[j] != p[i - 1])
j = ne[j];
if (p[j] == p[i - 1])
j++;
ne[i] = j;
}
//最后一个next不为0,并且len是len减去最长匹配长度的整数倍,即是前面子串的整数倍
return ne[len] != 0 && len % (len - ne[len]) == 0;//关键
}
}
Trie
208. 实现 Trie (前缀树)
可以用数组或者TrieNode , leetcode上面宫水三叶讲的很好
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
class Trie {
class TrieNode{
public boolean isEnd;
public TrieNode[] son = new TrieNode[26];
}
TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode p = root;
int len = word.length();
for(int i = 0; i < len; i++){
int x = word.charAt(i) - 'a';
if(p.son[x] == null)
p.son[x] = new TrieNode();
p = p.son[x];
}
p.isEnd = true;
}
public boolean search(String word) {
TrieNode p = root;
char[] arr = word.toCharArray();
int len = arr.length;
for(int i = 0; i < len; i++){
int x = arr[i] - 'a';
if(p.son[x] == null)
return false;
p = p.son[x];
}
return p.isEnd;
}
public boolean startsWith(String prefix) {
int len = prefix.length();
TrieNode p = root;
for(int i = 0; i < len; i++){
int x = prefix.charAt(i) - 'a';
if(p.son[x] == null)
return false;
p = p.son[x];
}
return true;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
单调队列
acwing模板题:
给定一个大小为n≤10^6的数组。有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。你只能在窗口中看到 k个数字。
每次滑动窗口向右移动一个位置。以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k is 3 。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k ,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
下面是acwing y总的模板:
#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int n,k;
int a[N],q[N];
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
int hh=0,tt=-1;
for(int i=0;i<n;i++){
//判断队头是否已经滑出窗口
if(hh<=tt&&i-k+1>q[hh]) hh++;
while(hh<=tt && a[q[tt]]>=a[i]) tt--;//把队列里所有比a[i]大的数都踢掉,它们将永无出头之日
q[++tt]=i;
if(i>=k-1) printf("%d ",a[q[hh]]);
}
puts("");
hh=0,tt=-1;
for(int i=0;i<n;i++){
//判断队头是否已经滑出窗口
if(hh<=tt && i-k+1>q[hh]) hh++;
while(hh<=tt && a[q[tt]]<=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}
239. 滑动窗口最大值
一个讲得挺好的博客: 单调队列
public class Solution {
static int[] queue = new int[100010];//数组模拟队列
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length, index = 0;
int[] ans = new int[len - k + 1];
//直接用双端队列类,慢
// Deque<Integer> queue = new LinkedList<>();//考虑到插入删除链表更高效,先用这个看看吧,结果与ArrayDeque一样
// for (int i = 0; i < len; i++) {
// //维持队列中窗口大小 = k,每进一个就要把窗口外的删除掉
// while (!queue.isEmpty() && queue.getFirst() < i - k + 1)
// queue.poll();
// //维持队列中索引对应的值为递减
// while(!queue.isEmpty() && nums[queue.getLast()] <= nums[i])
// queue.removeLast();
// queue.offer(i);
// if(queue.getLast() >= k - 1)
// ans[index++] = nums[queue.getFirst()];
// }
//数组模拟队列,快
int hh = 0, tt = -1;
for (int i = 0; i < len; i++) {
//上面while可以改为if,因为每次窗口向右移动一次,左边最多只会删除一个
if (hh <= tt && queue[hh] < i - k + 1)
hh++;
while (hh <= tt && nums[queue[tt]] <= nums[i])
tt--;
queue[++tt] = i;
if (queue[tt] >= k - 1)
ans[index++] = nums[queue[hh]];
}
return ans;
}
}
862. 和至少为 K 的最短子数组
public class 和至少为K的最短子数组 {
static int[] queue = new int[100010];//模拟队列
static long[] preSum = new long[100010];//记录前缀和
public int shortestSubarray(int[] nums, int k) {
int len = nums.length, ans = Integer.MAX_VALUE;
for (int i = 1; i <= len; i++){
if(nums[i - 1] >= k)
return 1;
preSum[i] = preSum[i - 1] + nums[i - 1];
}
int hh = 0, tt = -1;
//i从0开始枚举,非常重要,虽然前面前缀从1开始填的,但是0的前缀是0,如果漏掉0,
// 当队列中只有一个元素时,无法preSum[queue[tt]] - preSum[queue[hh]]计算是否大于等于K,并且无法计算全项值
//举个简单的例子:[2,-1,2],K=3,前缀[2,1,3],放到队列索引[2 3],然而队首尾差为3 - 1没到3,就输出了-1,显然是不对的
//所以以后前缀都写作多一项,0处为0
for (int i = 0; i <= len; i++) {
//维持一个单调递增的队列并且队尾与队首差小于K,首先明确队列中放数组的索引,当 当前要入队的值大于队尾或队空时直接入队
//如果要入队的值小于或等于队尾的值,一直删除队尾,直到空或者遇到大于当前值的停下,至于为什么删除,这是因为队尾比当前值大或等的一定
//永无出头之日,因为我们要选最小长度
//如果队尾元素减去队首元素大于等于K,那么开始检测删除队首后是否依然满足,对比记录下长度,一直删除队首直到不满足;否则continue
while (hh <= tt && preSum[i] <= preSum[queue[tt]])
tt--;
queue[++tt] = i;
//容易疏忽的点,如果队列中只有一个元素,tt == hh不可减 所以 hh < tt
while (hh < tt && preSum[queue[tt]] - preSum[queue[hh]] >= k)
ans = Math.min(ans, queue[tt] - queue[hh++]);
//如果上面没有从0开始,补充个下面就行了,解决无法计算全项值的问题
// if(preSum[queue[tt]] >= k)
// ans = Math.min(ans, queue[tt]);
}
return ans > 100000 ? -1 : ans;
}
}
并查集
introduction:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集合,我们只要找到了某个元素的的树根,就能确定它在哪个集合里。
并查集用在一些有 N 个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这个过程看似并不复杂,但数据量极大,若用其他的数据结构来描述的话,往往在空间上过大,计算机无法承受,也无法在短时间内计算出结果,所以只能用并查集来处理。
所谓并查集一共有两部分功能: 查 所在集合代表节点 : find() ; 并 合并两个集合(当前各自代表节点不同) union()
同时还需要有一个初始化函数 init() 起初将所有节点的根节点置为自身
基础模板引入题:
1. 问题描述:
有一个叫做“数码世界”奇异空间,在数码世界里生活着许许多多的数码宝贝,其中有些数码宝贝之间可能是好朋友,并且数码宝贝世界有两条不成文的规定:
第一,数码宝贝A和数码宝贝B是好朋友等价于数码宝贝B与数码宝贝A是好朋友
第二,如果数码宝贝A和数码宝贝C是好朋友,而数码宝贝B和数码宝贝C也是好朋友,那么A和B也是好朋友,现在给出这些数码宝贝中所有好朋友的信息,问:可以把这些数码宝贝分成多少组,满足每组中的任意两个数码宝贝都是好朋友,而且任意两组之间的数码宝贝都不是好朋友
样例:
input:
7 5
1 2
2 3
3 1
1 4
5 6
output:
3
import java.io.*;
public class 好朋友 {
static final int N = 110;
static int father[] = new int[N];//代表当前节点的一个父亲节点
static int hasSon[] = new int[N];//代表:如果当前节点是根节点,其集合中的元素数目,如果不是根节点则为0
public static void main(String[] args) throws IOException{
int n, m, a, b, setNum = 0;
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
n = (int)in.nval;
in.nextToken();
m = (int) in.nval;
init(n);
while(m-- != 0){
in.nextToken();
a = (int) in.nval;
in.nextToken();
b = (int) in.nval;
union(a, b);
}
for(int i = 1; i <= n; i++)
hasSon[find(i)]++;
for(int i = 1; i <= n; i++)
if(hasSon[i] != 0)
setNum++;
out.write(setNum + "");//输出集合数目,如果还要求输出每个集合中元素的数目,遍历输出hasSon[]就行
out.flush();
}
public static void init(int n) {
for (int i = 1; i <= n; i++)
father[i] = i;
}
public static int find(int x) {
if (x == father[x])
return x;
//递归法找根节点,并进行路径压缩,将路途中的节点直接指向根节点
int fa = find(father[x]);
return fa;
}
public static void union(int x, int y) {
int fatherX = find(x);
int fatherY = find(y);
if (fatherX != fatherY)
father[fatherX] = fatherY;
}
}
前缀和
一维
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l , r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m 。
第二行包含 n个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l ll 和 r rr,表示一个询问的区间范围。
输出格式
共 m行,每行输出一个询问的结果。
数据范围
1 ≤ l ≤ r ≤ n
1 ≤ n , m ≤ 100000
− 1000 ≤ 数 列 中 元 素 的 值 ≤ 1000
例子:
input:
5 3
2 1 3 6 4
1 2
1 3
2 4
output:
3
6
10
比较简单,acwing 模板
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int a[N],s[N];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
s[i]=s[i-1]+a[i];
}
while(m--){
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",s[r]-s[l-1]);
}
}
二维
输入一个 n行 m 列的整数矩阵,再输入 q qq 个询问,每个询问包含四个整数 x 1 , y 1 , x 2 ,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数 n , m , q
接下来 n 行,每行包含 m个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x 1 , y 1 , x 2 , y 2,表示一组询问。
输出格式
共 q 行,每行输出一个询问的结果。
数据范围
1 ≤ n , m ≤ 1000
1 ≤ q ≤ 200000
1 ≤ x 1 ≤ x 2 ≤ n
1 ≤ y 1 ≤ y 2 ≤ m
− 1000 ≤ 矩 阵 内 元 素 的 值 ≤ 1000
样例:
input:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
output:
17
27
21
acwing模板:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1010;
int n,m,q;
ll a[N][N],s[N][N];
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%lld",&a[i][j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
}
}
while(q--){
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
printf("%lld\n",s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);
}
}
DP
背包问题
01背包
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
二维dp数组
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[][] dp = new int[n + 1][m + 1];
int[] v = new int[n + 1];
int[] w = new int[n + 1];
for (int i = 1; i <= n; i++) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j] = dp[i - 1][j];
if(j >= v[i])
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
}
out.write(dp[n][m] + "");
out.flush();
}
}
优化:一维dp数组
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[] dp = new int[m + 1];
int[] v = new int[n + 1];
int[] w = new int[n + 1];
for (int i = 1; i <= n; i++) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j--){
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
out.write(dp[m] + "");
out.flush();
}
}
完全背包
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
二维
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[][] dp = new int[n + 1][m + 1];//dp[i][j]代表从前件物品中在背包最大容积为j时所选取的体积最大值
int[] v = new int[n + 1];
int[] w = new int[n + 1];
for (int i = 1; i <= n; i++) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j] = dp[i - 1][j];
if(j >= v[i])
dp[i][j] = Math.max(dp[i][j], dp[i][j - v[i]] + w[i]);
}
}
out.write(dp[n][m] + "");
out.flush();
}
}
一维优化
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[] dp = new int[m + 1];//dp[i][j]代表从前件物品中在背包最大容积为j时所选取的体积最大值
int[] v = new int[n + 1];
int[] w = new int[n + 1];
for (int i = 1; i <= n; i++) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
}
for(int i = 1; i <= n; i++){
for(int j = v[i]; j <= m; j++){ //注意这里与01背包的区别
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
out.write(dp[m] + "");
out.flush();
}
}
多重背包1
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
朴素做法
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[][] dp = new int[n + 1][m + 1];//dp[i][j]代表从前件物品中在背包最大容积为j时所选取的体积最大值
int[] v = new int[n + 1];
int[] w = new int[n + 1];
int[] s = new int[n + 1];
for (int i = 1; i <= n; i++) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
in.nextToken();
s[i] = (int) in.nval;
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
for(int k = 0; k <= s[i] && k * v[i] <= j; k++)
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
}
}
out.write(dp[n][m] + "");
out.flush();
}
}
一维
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[] dp = new int[m + 1];//dp[i][j]代表从前件物品中在背包最大容积为j时所选取的体积最大值
int[] v = new int[n + 1];
int[] w = new int[n + 1];
int[] s = new int[n + 1];
for (int i = 1; i <= n; i++) {
in.nextToken();
v[i] = (int) in.nval;
in.nextToken();
w[i] = (int) in.nval;
in.nextToken();
s[i] = (int) in.nval;
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= 1; j--){
for(int k = 1; k <= s[i] && k * v[i] <= j; k++)
dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
out.write(dp[m] + "");
out.flush();
}
}
多重背包2
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
提示:
本题考查多重背包的二进制优化方法。
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
此时数据量大,朴素做法会超时
二进制合并优化
合并后就是01背包了
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 22000;
int dp[N], v[N], w[N];
int main()
{
int cnt = 0;
int a, b, s;
int m, n;
cin >> m >> n;//物品种数,背包体积、
for(int i = 1; i <= m; i++)
{
cin >> a >> b >> s;
int k = 1;
while(k <= s)
{
cnt++;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if(s){
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
for(int i = 1; i <= cnt; i++)
for(int j = n; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
printf("%d", dp[n]);
return 0;
}
分组背包
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
import java.io.*;
public class Main{
public static void main(String[] args) throws IOException{
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, m;
in.nextToken();
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
int[][] dp = new int[n + 1][m + 1];//dp[i][j]代表从前件物品中在背包最大容积为j时所选取的体积最大值
int[] s = new int[n + 1];//每组中物品的数目
int[][] v = new int[n + 1][110];//存储每组物品的体积
int[][] w = new int[n + 1][110];//存储每组物品的价值
for(int i = 1; i <= n; i++){
in.nextToken();
s[i] = (int) in.nval;
for(int j = 1; j <= s[i]; j++){
in.nextToken();
v[i][j] = (int) in.nval;
in.nextToken();
w[i][j] = (int) in.nval;
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j] = dp[i - 1][j];
for(int k = 1; k <= s[i]; k++){
if(j >= v[i][k])
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i][k]] + w[i][k]);
}
}
}
out.write(dp[n][m] + "");
out.flush();
}
}
一维的优化就不写了,跟上面一样。只需要记住,一维优化时,如果用到上一层前面的价值,就要逆序更新,如果用的是本层的,正序
线性DP
数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
简单的线性DP,状态转移,如果左上右上都有,就是
dp[x][y] = Math.max(dp[x - 1][y], dp[x - 1][y - 1]) + arr[x][y];//x y为当前点的坐标
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n, ans = -Integer.MAX_VALUE;
in.nextToken();
n = (int) in.nval;
int[][] arr = new int[n + 1][n + 1], dp = new int[n + 1][n + 1];
//dp[x][y] 代表走到每个坐标(x,y)路径上的数字和的最大值
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
in.nextToken();
arr[i][j] = (int) in.nval;
}
}
for (int x = 1; x <= n; x++) {
for (int y = 1; y <= x; y++) {
//这里只是为了清晰这么写,也可以把除了1头上的都置为负无穷,这样就直接一个式子就行
if (x - 1 > 0 && y - 1 > 0 && y < x)//左上右上都有
dp[x][y] = Math.max(dp[x - 1][y], dp[x - 1][y - 1]) + arr[x][y];
else if(x == 1)//最上面的那个,左右上面都没有
dp[x][y] = arr[1][1];
else if(y - 1 < 1)//只有右上有
dp[x][y] = dp[x - 1][y] + arr[x][y];
else //只有左上有
dp[x][y] = dp[x - 1][y - 1] + arr[x][y];
if (x == n)
ans = Math.max(ans, dp[x][y]);
}
}
out.write(ans + "\n");
out.flush();
}
}
300. 最长递增子序列
状态转移方程 dp[i] = Max(dp[j] + 1) j < i dp[i] 代表以num[i]结尾的最长子序列
class Solution {
public int lengthOfLIS(int[] nums) {
int len = nums.length;
int[] dp = new int [len];
for(int i = 0; i < len; i++){
dp[i] = 1; //key,初始时只有当前位置一个数字,长度为1
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int ans =0;
for(int i = 0; i < len; i++){
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
最长公共子序列
给出两个长度为 n 的整数序列,求它们的最长公共子序列(LCS)的长度,保证第一个序列中所有元素都不重复。
注意:
第一个序列中的所有元素均不重复。
第二个序列中可能有重复元素。
一个序列中的某些元素可能不在另一个序列中出现。
输入格式
第一行包含一个整数 n。
接下来两行,每行包含 n 个整数,表示一个整数序列。
输出格式
输出一个整数,表示最长公共子序列的长度。
数据范围
1≤n≤106,
序列内元素取值范围 [1,106]。
输入样例1:
5
1 2 3 4 5
1 2 3 4 5
输出样例1:
5
输入样例2:
5
1 2 3 5 4
1 2 3 4 5
输出样例2:
4
这道题java用同样的方法会堆空间不足
1143. 最长公共子序列
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int ans = 0;
int len1 = text1.length(), len2 = text2.length();
char[] arr1 = text1.toCharArray();
char[] arr2 = text2.toCharArray();
int[][] dp = new int[len1 + 1][len2 + 1]; //注意用java写最好还是根据长度来创建数组,new时很费时间
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (arr1[i - 1] == arr2[j - 1]) //在leetcode,发现这里不用char数组存储每次都charAt查询对比用的时间更多
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
ans = dp[len1][len2];
return ans;
}
}
区间DP
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
按区间长度从小到大(因为每合并一大段区间都要在中间找分界点,将其分为两段小区间,两段小区间的最小体力和加上这一大段区间石子的总重量就是合并这个大区间的最小体力),求合并每段长度所需的最小体力(在中间找个分界点进行合并,枚举各个分界点,找到合并每段区间的最小值)
当然看到区间和,就要想到前缀和
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n;
in.nextToken();
n = (int) in.nval;
int[] arr = new int[n + 1];
int[][] dp = new int[n + 1][n + 1];
for(int i = 1; i <= n; i++){
in.nextToken();
arr[i] = (int) in.nval + arr[i - 1];//前缀和
}
//合并区间长度要从2开始,因为长度为1不耗费体力,就是0,dp[l][r]=0,如果从1开始,后面dp[l][r] = Integer.MAX_VALUE;就不对了
for (int len = 2; len <= n; len++) {
for (int left = 1; left + len - 1 <= n; left++) {
int l = left, r = left + len - 1;
dp[l][r] = Integer.MAX_VALUE;//因为后求最小,初始值为0,不置个大数会寄
//k为分界点,左边区间包含k,右边从k + 1开始
for (int k = left; k < r; k++) {
dp[l][r] = Math.min(dp[l][r], dp[l][k] + dp[k + 1][r] + arr[r] - arr[l - 1]);
}
}
}
out.write(dp[1][n] + "");
out.flush();
}
}
计数类DP
状态压缩DP
蒙德里安的梦想
求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。
例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
如下图所示:
2411_1.jpg
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 N 和 M。
当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205
起初这么写了,这是不对的,值得反思
package com.mylearn.acwing.DP;
import java.io.*;
import java.util.Arrays;
public class 蒙德里安的梦想 {
static final int N = 12;
static boolean[] st = new boolean[1 << N];//存储二进制数哪些满足没有连续奇数个0
static long dp[][] = new long[N][1 << N];//dp[i][j] 第i列,放置的状态j
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
首先这里不能放在外面求是否出现连续奇数0,因为每个n对应不同的位数
//求合法状态,即所以二进制数哪些满足没有连续奇数个0
for (int i = 0; i < (1 << N); i++) {
st[i] = true;
int cnt = 0;//记录连续0的个数
int j = i;
其次是这里,要把所有位都遍历了,然而这样对于高位0都遍历不了
while(j > 0){
if((j & 1) == 1){
//如果出现连续奇数0
if((cnt & 1) == 1){
st[i] = false;
break;
}
cnt = 0;//清零
}
else
cnt++;
j >>= 1;
}
//后面全是0没遇到1时
if((cnt & 1) == 1)
st[i] = false;
}
dp[0][0] = 1;//在第1~m列中才放小方格,前面第0列当然不放东西了,所以为1,其余为0
while (true) {
//每次对dp数组清零,第0列不用,因为每次不变
for(int i = 1; i < N; i++)
Arrays.fill(dp[i], 0);
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
if ((m | n) == 0)
break;
//优化下,因为复杂度为m*4^n,交换,保证n小,不影响结果,无非是旋转90度看
if(n > m){
n ^= m;
m ^= n;
n ^= m;
}
for (int i = 1; i <= m; i++){
for(int j = 0; j < 1 << n;j++){
for(int k = 0; k < 1 << n; k++){
//满足相邻两列没有在同一个放小方格 并且满足第i列没有连续奇数个空格
if( (j & k) == 0 && st[j | k])
dp[i][j] += dp[i - 1][k];
}
}
}
//最后一列当然不能放横着的小方格了,所以输出dp[m][0]
out.write(dp[m][0] +"\n");
out.flush();
}
}
}
改正后的
import java.io.*;
import java.util.Arrays;
public class Main {
static final int N = 12;
static boolean[] st = new boolean[1 << N];//存储二进制数哪些满足没有连续奇数个0
static long dp[][] = new long[N][1 << N];//dp[i][j] 第i列,放置的状态j
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
dp[0][0] = 1;//在第1~m列中才放小方格,前面第0列当然不放东西了,所以为1,其余为0
while (true) {
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
if ((m | n) == 0)
break;
//优化下,因为复杂度为m*4^n,交换,保证n小,不影响结果,无非是旋转90度看
if (n > m) {
n ^= m;
m ^= n;
n ^= m;
}
updateSt(n);//更新合法状态
for (int i = 1; i <= m; i++) {
for (int j = 0; j < 1 << n; j++) {
for (int k = 0; k < 1 << n; k++) {
//满足相邻两列没有在同一个放小方格 并且满足第i列没有连续奇数个空格
if ((j & k) == 0 && st[j | k])
dp[i][j] += dp[i - 1][k];
}
}
}
//最后一列当然不能放横着的小方格了,所以输出dp[m][0]
out.write(dp[m][0] + "\n");
out.flush();
//每次对dp数组清零,第0列不用,因为每次不变
for (int i = 1; i <= m; i++)
Arrays.fill(dp[i], 0);
}
}
public static void updateSt(int n) {
//求合法状态,即所以二进制数哪些满足没有连续奇数个0
for (int i = 0; i < (1 << n); i++) {
st[i] = true;
int cnt = 0;//记录连续0的个数
//对1~n位依次检查是否出现连续奇数0
for (int j = 0; j < n; j++)
if (((i >> j) & 1) == 1) {
//如果出现连续奇数0
if ((cnt & 1) == 1) {
st[i] = false;
break;
}
cnt = 0;//cnt清零
} else
cnt++;
//后面全是0没遇到1时
if ((cnt & 1) == 1)
st[i] = false;
}
}
}
看上面交换n m优化的效果,非常明显
最短Hamilton路径
给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数 n。
接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。
对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。
输出格式
输出一个整数,表示最短 Hamilton 路径的长度。
数据范围
1≤n≤20
0≤a[i,j]≤107
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18
枚举倒数第二个点是哪个点,用倒数第二个点来分类
import java.io.*;
import java.util.Arrays;
public class Main {
static final int N = 20;
static final int INF = 0x3f3f3f3f;
static int[][] arr = new int[N][N];//存储 i j之间的距离
static int[][] dp = new int[1 << N][N];//dp[i][j] i为二进制每位表示在走到节点j时所走过的点
public static void main(String[] args) throws IOException {
StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
in.nextToken();
int n = (int) in.nval;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
in.nextToken();
arr[i][j] = (int) in.nval;
}
//把dp置为INF,因为后面有用它比较最小值
for (int i = 0; i < 1 << n; i++)
Arrays.fill(dp[i], INF);
dp[1][0] = 0;//要知道,在起点时,走过的最短路径为0
//枚举所以已走过的点集状态
for (int i = 1; i < 1 << n; i++)
//遍历在该状态下,所以最终可能到达的节点
for (int j = 0; j < n; j++)
//判定j点是否走过
if ((i >> j & 1) == 1)
//白能力所以j 前面刚走过的节点 的所有情况
for (int k = 0; k < n; k++)
//把j删去,判定是否走过k点,如果走过就将其置为最后一个走到的点,进行状态转移
if (((i ^ (1 << j)) >> k & 1) == 1) //这里发现用位运算^ 比 - 快
dp[i][j] = Math.min(dp[i][j], dp[((i ^ (1 << j)))][k] + arr[k][j]);
//最后状态是在n-1节点,并且所有路都走过了
out.write(dp[(1 << n) - 1][n - 1] + "");
out.flush();
}
}
[蓝桥杯2021初赛] 回路计数
题目描述
蓝桥学院由21 栋教学楼组成,教学楼编号1 到21。
对于两栋教学楼a 和b,当a 和b 互质时,a 和b 之间有一条走廊直接相连,两个方向皆可通行,否则没有直接连接的走廊。
小蓝现在在第一栋教学楼,他想要访问每栋教学楼正好一次,最终回到第一栋教学楼(即走一条哈密尔顿回路),请问他有多少种不同的访问方案?
两个访问方案不同是指存在某个i,小蓝在两个访问方法中访问完教学楼i 后访问了不同的教学楼。
一个好文 (28条消息) 【状压DP】哈密顿回路问题_鱼竿钓鱼干的博客-CSDN博客
我的学习路径压缩前想的代码,用哈希表存储走过的状态