数据结构与算法

正在完善补充中~

基础算法

ps:(https://www.acwing.com/) 同上链接付费课程同步以及自己的完善笔记

0.Java语法的注意事项

打比赛时 应注意临界点的 例子 还应多测试自己的例子

1.读取数据

注意数据爆int的情况

防止读入、输出超时(滑动窗口题目时 数据范围106 普通Scanner 导致TLE)

使用Bufferread能加快读取速度,即

Scanner in = new Scanner(new BufferedInputStream(System.in));

int n = in.nextInt();

同时引入包

import java.util.Scanner;
import java.io.BufferedInputStream;

巧妙利用数组传递共享的参数

字符串 以及数组的操作

读入字符串

java的字符串数组若没有完全赋值 容易报空指针错

数组复制

Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。

copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值,例如:

import java.util.Arrays;

public class ArrayDemo {
	public static void main(String[] args) {
    	int[] arr1 = {1, 2, 3, 4, 5}; 
    	int[] arr2 = Arrays.copyOf(arr1, 5);
    	int[] arr3 = Arrays.copyOf(arr1, 10);
    	for(int i = 0; i < arr2.length; i++) 
        	System.out.print(arr2[i] + " "); 
    		System.out.println();
    	for(int i = 0; i < arr3.length; i++) 
        	System.out.print(arr3[i] + " ");
	}
} 
/*
输出结果
1 2 3 4 5 
1 2 3 4 5 0 0 0 0 0
*/

2.一些基本函数的转换

1.取最大值,最小值

Math.max() Math.min()

2.交换 swap

自写交换函数

int temp = arr[i];

arr[i] = arr[j];

arr[j] = temp;

3.科学计数法

int b3 = (int) 4_4_0.1e2;

尤其是在浮点数与0比较时 需考虑到0在计算机中存放时有误差 如在判等一个数是否等于0.0时 应用其的绝对值和1-e6(即一个较小的数)比较 如果小于则判定为0.0

java整型浮点型的求绝对值均为Math.abs

1.基础算法

1.排序

(1)快速排序

时间复杂度O(nlogn)

是不稳定的 但可以设立一个二维下标这样便能区分相同的数,使其变为稳定的

基本思路(分治思想) 实在不行也可用暴力 开辟两个额外空间 同时注意一下边界问题

1.确定分界点的值x(一般为:q[l] ,q[r] ,q[(l+r)/2], 随机数)。

2.把小于等于x的放在左边,把大于等于x的放在右边(较好的策略(核心思路):设置两个指针(i,j)分别指向数组开头和结尾。若i所指向的大于x则停下,否则继续自增走下去。若j所指向的小于x则停下,否则继续自减走下去。若二者都停下则交换。若i>j,则进行第三步操作。)

3.递归处理分界点左右两边

核心代码片段

private static void quicksort(int l,int r){
        if(l >= r) return;
        int i = l - 1;
        int j = r + 1;
        int k = q[r];//分界条件 为i时 不能为l + r >> 1(当l为 r - 1时) 和l 死循环
    //当分界条件为j时 不能为 l + r + 1 >> 1和 r
        while(i < j){
            do{
                i ++;
            }while(q[i] < k);
            do{
                j --;
            }while(q[j] > k);
            if(i < j) swap(i,j);
        }
        quicksort(l,i - 1);
        quicksort(i,r);
    }

快速选择算法 题目第K个数 时间复杂度O(n)

private static int quicksort(int l,int r,int k){
        if(l >= r) return q[r];
        int x = q[l],i = l - 1,j = r + 1;
        while(i < j){
            while(q[++ i] < x);
            while(q[-- j] > x);
            if(i < j) swap(i,j);
        }
        int sl = j - l + 1;
        if(sl >= k) return quicksort(l,j,k);
        else return quicksort(j + 1,r,k - sl);
    }
(2)归并排序

是稳定的 即对于两个相同的数排完序后位置与之前的保持不变

基本思想 分治法

1.确定分界点 mid=(l+r)/2

2.递归排序

3.归并 把两个数组合二为一

分析时空复杂度

关键代码

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

2.二分法

1.整数二分

注意边界问题 有时需要多次调试

原理:有单调性一定可以二分 没有单调性也可能二分 所以二分本质不是单调性

本质:分治思想 比如左半边满足这种性质check 右半边是不满足的

最后l和r会相等

最终结果如何:可以寻找左右两半边的边界

//两个模板
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

3.高精度

java和python语言中有直接对应的类

4.前缀和与差分

前缀和:对于数组arr[N] = {a1,a2,a3,a4,…} 可以计算前缀和数组s,si = si-1 + ai

5.双指针算法

先用暴力 再用双指针优化 O(N2) ——>O(N)

6.位运算

如何比较,第几位为1.可先>>右移k位至个位然后异或&1.

lowbit运算:返回第一个1 如100101000 则返回 1000

int lowbit(int x){
	return x & -x;
}

-x等于x的补码等于x求反+1

如:x = 100101000

~x + 1 = 011010111 + 1 = 011011000

7.离散化

数据量范围很大(如109)>105 但是很稀疏 能用到的数据量可能只有105 不然可以考虑前缀和 直接前缀和可能会超过内存限制

离散化:把所有用到的下标映射成从1开始的自然数

方法:

8.区间合并

1.按区间左端点排序2.

2.数据结构

1.链表与邻接表

实现方法有:结构体指针(每次都要new node 往往都是上万级的量非常慢 算法题中可能会超时)、STL容器

1.单链表

这里用数组来模拟单链表 以head表示头节点下标 以e[ i ]表示第i个节点的值 ne[ i ]表示第i个节点的指向第几个节点 空节点表示为-1 以idx表示当前用到了哪个点 下标可以自定义从0(或1)开始(不包含头节点head 此时头节点指向0(或1))

具体含义 见下列代码(注意实际题目中下标可能会出现问题)

int head,e[N],ne[N],idx;
//初始化操作 
void init(){
	head=-1;
	idx=0;//从0开始(不包括头节点)
}
//将节点插入到头节点后 
void add_to_head(int x){
    e[idx]=x;
	ne[idx]=head;
	head=idx;
	idx++;
}
//一般的插入操作 把值为x的节点插入到第k节点的后面 
void add(int x,int k){
    e[idx]=x;
	ne[idx]=ne[k];
	ne[k]=idx;
	idx++;	
}
//删除第k节点后的那个节点 
void remove(int k){
	//这里不用考虑内存泄漏 
	ne[k]=ne[ne[k]];
}

删除头节点可表示为 head=ne[head] 具体解释可参照 数据结构(一)45:50

2.双链表

部分定义同上 记head头节点下标为0 记tail尾节点下标为1 l[N]表示左边的点 r[N]表示右边的点

int e[N],l[N],r[N],idx;
void init(){
	r[0]=1,l[1]=0;
	idx=2;
} 
//在第k节点的右边插入节点 
void add(int k,int x){
	e[idx]=x;
	r[idx]=r[k];
	l[idx]=k;
	l[r[k]]=idx;
	r[k]=idx;
	idx++;	
}
void remove(int k){
	r[l[k]]=r[k];
	l[r[k]]=l[k];
}

2.栈与队列

实现方法:同上

1.栈 队列

具体可见视频

2.单调栈 单调队列

思路:先用暴力 再找规律找到单调性 再优化 比如有些值永远不可能使用

1.单调栈

题目描述:给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i个数表示第 i个数的左边第一个比它小的数,如果不存在则输出 -1。

数据范围

1≤N≤105
1≤数列中元素≤109

输入样例:

5
3 4 2 7 5

输出样例:

-1 3 -1 2 2

题目分析:可以用暴力做法 两层循环时间复杂度为O(n2)

简化思路:有些数字是永远不会用上 例如有以下数据 a1 a2 a3 …ai 以ai为中心 那么若有x<y<i 且ax >ay以及ax , ay <ai那么ax必不会被用 于是可删除ax 最终形成单调上升的数列 然后放在栈中从栈顶开始与ai 比较 如果stk[tt] >= ai 那么tt-1 比较下一个直到出现比ai 小的

代码如下:

#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;
        if (!tt) printf("-1 ");
        else printf("%d ", stk[tt]);
        stk[ ++ tt] = x;
    }
    return 0;
}

3.kmp

时间复杂度为0(N) 因为next数组总共最多减m次(每跳一次就最少减一次)

思路仍然是先暴力再考虑优化

106长的字符串读入超时 输出容易超时 需用BufferedReader BufferedWriter

4.Trie

高效存储和查找字符串的数据结构

对于整数的应用可考虑 用二进制表示 0 、 1串

5.并查集

快速处理

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合

暴力做法 合并O(N) 询问O(1)

并查集基本原理 每个集合用一棵树表示 树根的编号就是整个集合的编号 每个节点存储它的父节点 p[x] 表示x的父节点

6.堆

7.Hash表

将一个庞大的数据(010<sup>9</sup>)映射到小数据范围之间(0105)

8.STL

3.搜索与图论

1.DFS和BFS

DFS BFS 对比

数据结构层面空间复杂度层面
DFSstack 栈O(h) h为高度无最短路径概念
BFSqueue 队列O(2h) h为高度有最短路径概念

求一般树的直径(找到两个点之间的边数最大):任取一点为起点,找到里该点最远的点u(bfs,dfs都可推荐bfs不容易爆栈)。再找距离u最远的点v。uv之间即为所求。证明:u一定为最长的直径的一个端点。分情况讨论1.au与直径不相交2.au与直径相交。

1.DFS 深度优先搜索

对于DFS最重要的是理清搜索顺序 每个DFS一定对应一棵搜索树 不能存在环

1.排列数字

1.问题描述

给定一个整数 n,将数字 1∼n排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1≤n≤7

输入样例:

3

输出样例:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1 

2.问题分析

假设为3个数 1、2、3 写出其全排列

可按先第一位 确定第一位后 再第二位 确定第二位后再第3位的顺序

从递归结束后 回溯的时候 一定要恢复现场

XXX
1XX
2XX
3XX
12X
13X
123
132
21X
23X
213
231
32X
31X
321
312

3.代码

#include<iostream>
using namespace std;
const int N=10;
int n;
//保存路径
int path[N];
bool st[N];//true表示该数字已经用过
void dfs(int u){
    //u代表已有位数 最初为0
	//当u==n时 说明位数已经填满
	if(u==n){
		for(int i=0;i<n;i++) cout<<path[i]<<" ";
		printf("\n");
		return;
	}	
    for(int i=1;i<=n;i++)
	//没用过这数字时
		if(!st[i])
		{
			//填上数字i 并记录被用过 再往下走
			path[u]=i;
			st[i]=true;
			dfs(u+1);
			//走完后 回溯应恢复现场
			st[i]=false;
		}
}
int main(){
	cin>>n; 
	dfs(0);
	return 0;
}
2.n-皇后问题

1.题目描述:

n皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。现在给定整数 n,请你输出所有的满足条件的棋子摆法。

2.思路:从第0行开始枚举。每一行又枚举每一列。状态改变时,使用三个状态数组表示,即列,对角线,负对角线。

3.代码:

import java.util.*;

public class Main{
    static int N = 11,n;
    static boolean[] col = new boolean[N];//表示每列的状态
    static boolean[] dg = new boolean[2 * N];//表示正对角线状态
    static boolean[] udg = new boolean[2 * N];//表示负对角线状态
    static char[][] ans = new char[N][N];//存储状态
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        for(int i = 1;i <= n;i ++)
            for(int j = 1;j <= n;j ++)
                ans[i][j] = '.';
        dfs(1);//从第一行开始枚举
    }
    private static void dfs(int u){
        if(u == (n + 1)){
            for(int i = 1;i <= n;i ++)
            {
                for(int j = 1;j <= n;j ++)
                     System.out.print(ans[i][j]);
                System.out.println("");     
            }
            System.out.println("");
            return;
        }
        
        for(int i = 1;i <= n;i ++)//枚举列
            if(!col[i] && !udg[u + i - 1] && !dg[n - u + i]){
                ans[u][i] = 'Q';
                col[i] = udg[u + i - 1] = dg[n - u + i] = true;
                dfs(u + 1);
                col[i] = udg[u + i - 1] = dg[n - u + i] = false;//还原 让同一行下一列枚举 有在这一行上的初始状态
                ans[u][i] = '.';//一定要还原
            }
        
    }
}
2.BFS 宽度优先搜索

结果必为最短路(当边权为1时);边权为1时才能用BFS,否则使用其他专用的求最短路算法。会发现最短路类似于DP,实质上DP是特殊的最短路(DP可看作没有环路的最短路)。

步骤:queue初始化;while(队列不空):取队头,扩展队头。

2.树与图的遍历:拓扑排序

图的表示:邻接矩阵,十字链表

int h[N],int e[N],int ne[N],int idx;
//b指向a的有向边
void add(int a,int b)
{
    e[idx] = a;
    ne[idx] = h[b];
    h[b] = idx ++;
}

3.最短路

难点:转化成最短路模型 如何建图 确立边、点 下列各经典算法的证明可自行研究

其中对于有向图和无向图 有向图相当于为确定从a到b以及确定从b到a 而无向图相当于只确定从a到b 所以无向图可理解为特殊有向图

import java.util.*;
class Node{
    int x,y;
    public Node(int x,int y){
        this.x = x;
        this.y = y;
    }
}
public class Main{
    static int N = 110,n,m;
    static Queue<Node> q = new LinkedList<>();//存点的队列(可以扩展成各种各样的状态 参考八重码)
    static int[][] g = new int[N][N];//存图
    static int[][] d = new int[N][N];//表示该点到0,0点的距离
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        for(int i = 0;i < n;i ++)
            for(int j = 0;j < m;j ++)
                g[i][j] = sc.nextInt();
        
        System.out.println(bfs());
    }
    private static int bfs(){
        int[] x = new int[]{-1,0,1,0};//左上右下
        int[] y = new int[]{0,-1,0,1};//左上右下
        for(int i = 0;i < N;i ++) Arrays.fill(d[i],-1);
        d[0][0] = 0;
        q.offer(new Node(0,0));
        while(q.size() != 0){
            Node head = q.poll();
            for(int i = 0;i < 4;i ++)
            {
                int x_t = head.x + x[i],y_t = head.y + y[i];
                if(x_t >= 0 && x_t < n && y_t >= 0 && y_t < m && g[x_t][y_t] == 0 && d[x_t][y_t] == -1){
                    q.offer(new Node(x_t,y_t));
                    d[x_t][y_t] = d[head.x][head.y] + 1;
                }
                
            }
            
        }
        
        return d[n - 1][m - 1];
    }
}
1.单源最短路(求从某点到某点的最短路)
1.所有边权都是正数
1.朴素Dijkstra

时间复杂度:(O(n2)) 适用稠密图(一个图中 ,顶点数n,边数m 当n2>>m时,我们称之为稀疏 当m相对较大时 我们称为稠密)

题目描述:给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围

1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。

输入样例:

3 3
1 2 2
2 3 1
1 3 4

输出样例:

3

思路:记S[i]存放已经确定最短距离的点

步骤:1)初始化距离 dist[i](第i号点到起点的距离)dist[1]=0 dist[i]=+∞(其他所有点初始化为一个极大正数)

2)循环n次 每一次 for( i 0~n){不在S中的,距离最近的点放在T中;把T加到S中;用T更新其他点的距离(即从点t出去的距离能不能更新其他路径数据)}

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510;//这里为稠密图考虑邻接矩阵

int n,m;
int g[N][N];
int dist[N];//表示距离
bool s[N];//表示是否每个点已经确定了

int dijkstra()
{
    //初始化距离
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    for(int i=0;i<n;i++)
    {
        //t表示还未确定最短路径的点
        int t=-1;
        for(int j=1;j<=n;j++)
            //如果j未被确定并且距离也最小 则把j放到数组s[]中
            if(!s[j]&&(t==-1||dist[t]>dist[j]))
                t=j;
        s[t]=true;
       //用1到t的距离来更新1到j的距离 即判断g[1][t]+g[t][j]与g[1][j]的大小
        for(int j=1;j<=n;j++)
            dist[j]=min(dist[j],dist[t]+g[t][j]);
        
    }
    if(dist[j]==0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    scanf("%d%d",&n,&m);
    //去重边、去自环
    memset(g,0x3f,sizeof g);//将数组全部赋值为无穷大
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b]=min(g[a][b],c);//去重边
    }
    int t=dijkstra();
    printf("%d\n",t);
    return 0;
}
2.堆优化版的Dijkstra

利用C++特性

时间复杂度:(O(mlogn)) 适用稀疏图(一个图中 ,顶点数n,边数m 当n2>>m时,我们称之为稀疏 当m相对较大时 我们称为稠密)

2.存在负权边
1.Bellman-Ford

时间复杂度:(O(nm))

思路:for循环n次 每一次循环所有边

/*****************伪代码*****************************/
for n次
    备份
    for 所有边 a,b,w
        dist[b]=min(dist[b],dist[a]+w);//三角不等式

代码如下:

#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;
const int N=510,M=10010;
int n,m,k;
int dist[N],backup[N];

struct Edge
{
    int a,b,w;
}edges[M];

int bellman_ford()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
        
    for(int i=0;i<k;i++)
    {
        //备份
        memcpy(backup,dist,sizeof dist);
		for(int j=0;j<m;j++)
        {
            int a=edges[j].a,b=edges[j].b,w=edges[j].w;
            dist[b]=min(dist[b],backup[a]+w);
        }
    }
    if(dist[n]>0x3f3f3f3f/2) return -1;
    return dist[n];
}

int main()
{
    scanf("%d%d%d",&n,&m,&k);
    for(int i=0;i<m;i++)
    {
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i]={a,b,w};
    }
    int t=bellman_ford();
    if(t==-1) puts("impossible");
    else printf("%d\n",t);
    
    return 0;
}
2.SPFA

时间复杂度:(一般O(m) 最坏O(nm) 是对Bellman-Ford算法的优化)

2.多源汇最短路(起点 终点多个)
Floyd

时间复杂度:(O(n3))

4.最小生成树

5.二分图:染色法、匈牙利算法

4.数学知识

1.质数

质数的判定

——试除法 (时间复杂度必为O(sqrt(N)))

for(int i = 2;i<n;i++) if(n%i == 0) return;时间复杂度为O(N)

试除法优化for(int i = 2;i<=n/i;i++) return;时间复杂度为O(sqrt(N))

分解质因数

——试除法

可以考虑枚举从2~N的所有数 没枚举一个 若能整除 则直到将其整除干净为止 容易TLE

优化 思路:一个数最多有一个大于sqrt(N)的质数 (若有两个大于sqrt(N)的质数 则其二者相乘大于N 矛盾)

则只需让i枚举到sqrt(N)即可 最后判断剩余的数是否大于1即可

private static void divide(int n){
        for(int i = 2; i <= n/i ; i++){
            int s = 0;
            while( n % i == 0){
                s++;
                n/= i;
            }
            if( s != 0) System.out.println(i+" "+s);
        }
        if(n > 1)  System.out.println(n+" "+1);
    }

时间复杂度为最坏O(sqrt(N))最好O(logN)(当N = 2k

筛质数

——从1~N筛选出质数

首先考虑逐个划去各个数的倍数对于2划去4、6、8…对于3划去6、9、12…对于4划去8、12、16…剩下的即为质数

循环次数为 (N/2+N/3+N/4+…+N/N)当N—>+∞ (1+N/2+N/3+N/4+…+N/N)—>ln(N+1)+e

证明ln(1+) <

2.约数

确定所有约数

——试除法O(sqrt(N))

约数个数

约数之和
最大公约数

——欧几里得算法(辗转相除法)

3.欧拉函数

欧拉函数定义:求1~N中与N互质的个数(包括1 如6互质数为1、5)

欧拉定理

费马定理

4.快速幂

已知a,b,p均≤2×109 求ab 除p的余数 暴力求解 循环b次 超时且超出大小 这里考虑将b以2进制的方式写出 如(1101)2

ab = a * a22 * a23 这里将时间复杂度优化为 O(k)—>O(logk)

快速幂模板

    private static long quickmod(long a,long b,long p){
        long res = (long)1;
        while(b > 0){
            if((b & 1) == 1) res = (res * a) % p;//以b的2进制方式
            b = b >> 1;
            a = (a * a) % p; //依次为a a^2 a^4 a^8 a^16...
        }
        return res;
    }

应用:快速幂求逆元

给定 n 组 ai,pi,其中 pi 是质数,求 ai 模 pi 的乘法逆元,若逆元不存在则输出 impossible

注意:请返回在 0∼p−1 之间的逆元。

乘法逆元的定义

若整数 b,m互质,并且对于任意的整数 a,如果满足 b|a,则存在一个整数 x,使得 a/b≡a×x(mod m),则称 x 为 b 的模 m 乘法逆元,记为 b−1(mod m)。
b 存在乘法逆元的充要条件是 b 与模数 m 互质。当模数 m 为质数时,bm−2 即为 b 的乘法逆元。

输入格式

第一行包含整数 n。

接下来 n 行,每行包含一个数组 ai,pi,数据保证 pi是质数。

输出格式

输出共 n 行,每组数据输出一个结果,每个结果占一行。

若 ai 模 pi 的乘法逆元存在,则输出一个整数,表示逆元,否则输出 impossible

数据范围

1≤n≤105
1≤ai,pi≤2∗109

输入样例:

3
4 3
8 5
6 3

输出样例:

1
2
impossible

思路:费马小定理 如果p是一个质数,而整数a不是p的倍数,则有ap-1 ≡ 1(mod p)

代码:

import java.util.*;

public class Main{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        while(n > 0)
        {
            n --;
            long a = sc.nextLong();
            long p = sc.nextLong();
            if((a % p) == 0) System.out.println("impossible");
            else{
                long res = quickmod(a,p);
                System.out.println(res);   
            }
        }
        
    }
    private static long quickmod(long a,long p){
        long res = (long)1,b = p - (long)2;
        while(b > 0){
            if((b & 1) == 1) res = (res * a) % p;
            b = b >> 1;
            a = (a * a) % p;
        }
        return res;
    }
}

5.扩展欧几里得算法

6.中国剩余定理

7.高斯消元

有唯一解 有无数解 无解 知识内容参考线代知识

8.组合计数

9.容斥原理

韦恩图两个圆、三个圆的面积推广至四个圆甚至更多。等价于求N个集合不同元素的方法。

Cn0+Cn1 +Cn2+Cn3+…+Cnn=从n个数里选任意个数的方法,即每种数都有选和不选=2n

Ck1-Ck2 +Ck3-Ck4+…+(-1)k-1Ckk=1

对于能同时被k个质数整除的个数=[N/p1p2p3…pk]

10.简单博弈论

1.公平组合游戏ICG

若一个游戏满足:1.由两名玩家交替行动;2.在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;3.不能行动的玩家判负则称该游戏为一个公平组合游戏。NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足icg规则。

这种无论先手、后手状态都采取最佳策略(仍然是找规律解决此类问题)。必胜必败状态指先手必胜(即后手必败,一定能变为必败状态)或先手必败(先手无论怎么操作都必败)。

2.Mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:mex(S)=min(x),x属于自然数,且x不属于S

3.SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1 ,y2 ,y3 ,…yk,定义SG(x)为x的后继节点y1 ,y2 ,y3 ,…yk的SG函数值构成的集合再执行mex(S)运算的结果,即:SG(x)=mex({SG(y1), SG(y2), … ,SG(yk)})

特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G)=SG(s)

SG(x)=0必胜

SG(x)≠0必败

Nim游戏

题目描述:给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 n。

第二行包含 n 个数字,其中第 ii 个数字表示第 ii 堆石子的数量。

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

1≤n≤105,
1≤每堆石子数≤109

输入样例:

2
2 3

输出样例:

Yes

解题思路:设有n堆石子 每堆石子个数分别为a1 ,a2 ,a3 …an

若 a1⊕a2 ⊕a3 ⊕ …⊕an =0 则先手必败;若a1⊕a2 ⊕a3 ⊕ …⊕an ≠0则先手必胜

下证此结论正确

集合-Nim游戏
台阶-Nim游戏

5.动态规划

时间复杂度 = 状态数量 * 转移数量

ps:常用dp模型。dp的优化往往是对代码和计算函数的等价变形。

特点或解题关键:

1.考虑解题维度

2.用子问题定义状态 写出状态转移方程

DP优于DFS在于能减少很多不必要的状态 动态规划 用一个状态表示所有最值 DFS是遍历所有状态

1.背包问题

(B站详细背包问题大全 up主:大雪菜 背包九讲专题_哔哩哔哩_bilibili具体文章 见 dd大牛的背包九讲 https://www.cnblogs.com/jbelial/articles/2116074.html)

1.01背包问题

1.问题描述:有N个物体,一个容量为V的背包。每件物体价值为wi,体积为vi,每件物品只能用一次。求在装不超过背包容量的前提下,使物品的价值和最大。

2.dp问题分析(可通用)

Dp
状态表示
状态计算
集合
属性
所有选项
条件
只从前i个物品中选择
总体积不超过容量j
min,max,number
集合划分

思路:将该dp问题作为二维集合分析f(i,j)(f(i,j)表示最大价值)。i表示可选物品(编号已默认确定),j表示背包体积。f(N,V)即为所求。

f(i,i)=max{不含第i个物品的最大价值化选择,含第i个物品的最大价值化选择}=max{f(i-1,j),f(i-1,j-vi)+wi}。逐次递归。

3.代码

#include<iostream>
#include<algorithm> 
using namespace std;
const int N=1010;
//n为物品数量 m为背包容量
int n,m;
//v[i]为物品体积,w[i]为物品价值
int v[N],w[N];
//二维考虑二维数组(连续性 如i,j为连续性变化) 初始化默认为元素均为0
int f[N][N];
int main(){
	cin>>n>>m;
    //i从1开始(0个物品没有意义) j从容量为0开始
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    //调换两个for循环的顺序是一致的
	for(int i=1;i<=n;i++)
		for(int j=0;j<=m;j++)
		{
            //f[i][j]=f[i-1][j]是必有的 只有当j>=v[i]才有f[i-1][j-v[i]]+w[i])
			f[i][j]=f[i-1][j];
			if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
		}
	cout<<f[n][m]<<endl;
	return 0;
}

简化代码

1)初简化 可删掉条件直接将j从v[i]处开始计算(错误的做法) 示例:n=4 m=4;4 1;2 9;4 7;2 8; 应为17 实为9;

原因 :直接从j=v[i]处开始 中间值没有赋值导致错误

	for(int i=1;i<=n;i++)
		for(int j=v[i];j<=m;j++)
		{
            f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
		}

2)二维转换为一维数组

简化思路:在计算中,i只用于i-1所以考虑省略,用滚动数组即对于此二维数组来说,每一行最多考虑上一行的值。

ps:若直接转化为一维 下面代码不等价上式代码

由于f[j]是从小到大的增加 f[j-v[i]]实际上为 f[i] [j-v[i]]不等价于f[i-1] [j-v[i]]

	for(int i=1;i<=n;i++)
		for(int j=v[i];j<=m;j++)
		{
            f[j]=max(f[j],f[j-v[i]]+w[i]);
		}

解决办法:考虑调换j的顺序使之递减。此时当开始计算某i行的f[j]时 此时的第j-v[i]列还未计算 而此时的f[j-v[i]]保存的是第i-1行的值

#include<iostream>
#include<algorithm> 
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    //不断更新f[j]
	for(int i=1;i<=n;i++)
		for(int j=m;j>=v[i];j--)
		{
            f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	cout<<f[m]<<endl;
	return 0;
}
2.完全背包问题

1.问题描述:有N个物体,一个容量为V的背包。每件物体价值为wi,体积为vi,每件物品有无限个。求在装不超过背包容量的前提下,使物品的价值和最大。

2.问题分析

将该dp问题作为二维集合分析f(i,j)(f(i,j)表示最大价值)。i表示可选物品(编号已默认确定),j表示背包体积。f(N,V)即为所求。

f(i,j)=max{不含第i个物品的最大价值化选择,选1个第i个物品的最大价值化选择,选1个第i个物品的最大价值化选择}=max{f(i-1,j),f(i-1,j-vi)+wi}。逐次递归。

3.代码

1)最朴素代码

#include<iostream>
#include<algorithm> 
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
		for(int j=0;j<=m;j++)
		 	for(int k=0;k*v[i]<=j;k++)
                //不断更新f[i][j] 先比较f[i][j]和f[i-1][j] 保留最大值 便不断比较取1个,2个,3个.....的情况
		 		f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
	cout<<f[n][m]<<endl;
	return 0;
}

缺点:三重循环 最坏时间复杂度T(n* m*j) (当v[i]=1时)时间太长

2)简化代码

思路:删去循环 删除与k的联系 找和前面数组元素之间的联系 用已知的元素来计算

观察不难发现

f[i] [j]=Max{f[i-1] [j], f[i-1] [j-v[i]]+w[i], f[i-1] [j-2* v[i]]+2 w[i],* f[i-1] [j-3* v[i]]+3 w[i]*,…}

f[i] [j-v[i]]=Max{ f[i-1] [j-v[i]], f[i-1] [j-2 v[i]]+w[i], f[i-1] [j-3 v[i]]+2 w[i]*, …}

易得f[i] [j]=Max{f[i-1] [j], f[i] [j-v[i]]+w[i]}

	for(int i=1;i<=n;i++)
		for(int j=0;j<=m;j++){
			f[i][j]=f[i-1][j];
		 	if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
		 	}

进一步简化 二维转一维(ps :这里就没有01背包问题所考虑的需要调换顺序 需要的就是第i行的元素)

#include<iostream>
#include<algorithm> 
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
		for(int j=v[i];j<=m;j++)
			 f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[m]<<endl;
	return 0;
}
3.多重背包问题

1.问题描述:有 N 种物品和一个容量是 V的背包。第 i种物品最多有 si 件,每件体积是 vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

2.问题分析:同上完全背包问题 只是多加个限制k<=s[i]

3.代码

暴力解法

#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int f[N][N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
	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++)
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
	cout<<f[n][m]<<endl;
	return 0;
} 

2)二进制优化

数据范围

0<N≤1000
0<V≤2000
0<vi,wi,si≤2000

当数据范围扩大 明显会超时。若考虑使用完全背包问题的优化思路,则

f[i] [j]=max{f[i-1] [j] ,f[i-1] [j-v[i]]+w[i] ,f[i-1] [j-2* v[i]]+2* w[i] ,f[i-1] [j-3* v[i]]+3* w[i] …f[i-1] [j-s[i]* v[i]]+s[i]* w[i]}

f[i] [j-v[i]]=max{f[i-1] [j-v[i]] ,f[i-1] [j-2* v[i]]+ w[i] ,f[i-1] [j-3* v[i]]+2* w[i] …f[i-1] [j-s[i]* v[i]]+(s[i]-1)* w[i] ,f[i-1] [j-(s[i]+1)* v[i]]+s[i]* w[i] } 无法求解 比如当v[i] * s[i] < j 时就不能用完全背包来优化

优化思想:设第i种物品有s[i]个如1023(逐个枚举k 时间复杂度过大)可以将其分组每组个数为1、2、4、8、16、…512 不难发现可以在保证每组只用一次的情况下表示出0~1023的所有数字(如:1和2可表示出0-3 则1、2、4可表示出0-7) 更一般的对任意s 可将其拆分为s=1+2+4+…+2k-1+C(C=s-2k+1) 最后将他们每组都当作一种物品(体积为2i* a 价值为2i* b),最后采用01背包问题解决(比如将这两种物品进行组合 其实为凑出0-s中的某个个数的同种物品)。

代码如下

#include<iostream>
#include<algorithm>

using namespace std;
const int N=25000,M=2010;
//扩展思路2000*log(2000)约为25000
int n,m;
int v[N],w[N];
int f[N];

int main(){
	cin>>n>>m;
    //cnt作为标志 扩展物品数组
	int cnt=0;
	for(int i=1;i<=n;i++){
        //a为体积 b为价值 s为个数
		int a,b,s;
		cin>>a>>b>>s;
		int k=1;
		while(k<=s){
			cnt++;
			v[cnt]=k*a;
			w[cnt]=k*b;
			s-=k;
			k*=2;
		}
		if(s>0){
			cnt++;
			v[cnt]=s*a;
			w[cnt]=s*b;
		}
	}
    //最后采用01背包问题解决
	for(int i=1;i<=cnt;i++)
		for(int j=m;j>=v[i];j--)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[m]<<endl;
	return 0;
} 

时间复杂度从T(N* V* S)=>T(N* V* logS)

3)单调队列优化 滑动窗口

4.分组背包问题

1.问题描述:有 N 组物品和一个容量是 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

2.问题分析:

定义f[i , j]为从前i组中选择 背包容量为j。

f(i , j)=Max{f(i-1 , j) , 第i组选第一个的最大价值化选择,第i组选第2个的最大价值化选择,…,第i组选第j个的最大价值化选择}

其中第i组选第k个的最大价值化选择的状态转移方程=f[i-1] [j-v[i] [k]]+w[i] [k]

ps:使用一维数组时 此时转移方程依赖上一行的元素 所以j应该从大到小递减 若是本行则从小到大

3.代码

#include<iostream>
#include<algorithm>

using namespace std;
const int N=110;

int n,m;
//s[i]表示第i组物品个数
int v[N][N],w[N][N],s[N];
int f[N];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>s[i];
		for(int j=0;j<s[i];j++){
			cin>>v[i][j]>>w[i][j];
		}
	}
	for(int i=1;i<=n;i++)
        //这里没加j的限制
		for(int j=m;j>=0;j--)//j 和 k 调换顺序导致结果不一致 
            //调换后 含义改变 从第k组选出体积不大于j的最大价值
			for(int k=0;k<s[i];k++){
                //把j的限制放在此处
                if(v[i][k]<=j)	f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
			}
		
	cout<<f[m]<<endl;
	return 0;
} 
5.混合背包问题

1.问题描述:有 N 种物品和一个容量是 V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,N,V 用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i种物品的体积、价值和数量。

  • si=−1 表示第 i 种物品只能用1次;
  • si=0表示第 i 种物品可以用无限次;
  • si>0 表示第 i种物品可以使用 si次;

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi≤1000
−1≤si≤1000

输入样例

4 5
1 2 -1
2 4 1
3 4 0
4 5 2

输出样例:

8

2.问题分析

[思路一] 将三者01背包问题 完全背包问题 混合背包问题 全部转化为01背包问题。类似于二进制优化,将它们全部打散作为01背包。对于完全背包问题,即无限个个数时,也会由于不能超过背包体积而可看作有限个个数,所以这里也可以考虑二进制优化。

3.代码

[思路一]

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1000000;
int n,m;
int v[N],w[N];
int f[N];

int main(){
	cin>>n>>m;
	int cnt=0;
	for(int i=1;i<=n;i++){
		//拆分成01背包问题  
		int a,b,c;
		cin>>a>>b>>c;
		//01背包 
		if(c==-1){
			cnt++;
			v[cnt]=a;
			w[cnt]=b;
		}
		//完全背包 前提条件为总和不大于m
		if(c==0){
			int k=1;
			while(a*k<=m){
			cnt++;
			v[cnt]=a;
			w[cnt]=b;
			k++;
			}
		}
		//多重背包 
		if(c>0){
			int k=1;
			while(k<=c){
			cnt++; 
			v[cnt]=k*a;
			w[cnt]=k*b;
			c-=k;
			k*=2;
		}
		if(c>0){
			cnt++;
			v[cnt]=c*a;
			w[cnt]=c*b;
		}
		}
	}
	//转化成一维背包 
	for(int i=1;i<=cnt;i++)
		for(int j=m;j>=v[i];j--)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[m]<<endl;
	return 0;
}
 

最坏时间复杂度 T(2* n* m)

思路一优化

for(int i=1;i<=n;i++){
		//拆分成01背包问题  
		int a,b,c;
		cin>>a>>b>>c;
		//01背包 
		if(c==-1){
			cnt++;
			v[cnt]=a;
			w[cnt]=b;
		}
		//多重背包 
		if(c>=0){
			if(c==0){
			int t=1; 
			while(a*t<=m){
				t++;
			}
			c=t-1;	
			}
			int k=1;
			while(k<=c){
			cnt++; 
			v[cnt]=k*a;
			w[cnt]=k*b;
			c-=k;
			k*=2;
		}
		if(c>0){
			cnt++;
			v[cnt]=c*a;
			w[cnt]=c*b;
		}
		}
	}

最坏时间复杂度T(2* n* m)

6.二维费用的背包

1.问题描述

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。

输入格式

第一行三个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i件物品的体积、重量和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000

2.问题分析

1.所谓二维 即物品受两种代价的限制。单纯增加一维的限制

3.代码

1)最朴素的代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010,M=110;
int n,vk,mk;
int v[N],w[N],m[N];
int f[N][M][M];

int main(){
	cin>>n>>vk>>mk;
	for(int i=1;i<=n;i++) cin>>v[i]>>m[i]>>w[i];
	for(int i=1;i<=n;i++)
		for(int j=0;j<=vk;j++)
			for(int k=1;k<=mk;k++){
				f[i][j][k]=f[i-1][j][k];
				if(j>=v[i]&&k>=m[i]) f[i][j][k]=max(f[i][j][k],f[i-1][j-v[i]][k-m[i]]+w[i]);
			}
	cout<<f[n][vk][mk]<<endl;
	return 0;
}

优化

import java.util.*;

public class Main{
    static int N = 1010,V = 110,M = 110,n,v,m;
    static int[] wi = new int[N];
    static int[] vi = new int[N];
    static int[] mi = new int[N];
    static int[][] f = new int[V][M];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        v = sc.nextInt();
        m = sc.nextInt();
        for(int i = 1;i <= n;i ++){
            vi[i] = sc.nextInt();
            mi[i] = sc.nextInt();
            wi[i] = sc.nextInt();
        }
        for(int i = 1;i <= n;i ++ )
            for(int j = v;j >= vi[i];j -- )
                for(int k = m;k >= mi[i];k -- )//调换这两层循环的顺序 结果不变
                    f[j][k] = Math.max(f[j - vi[i]][k - mi[i]] + wi[i],f[j][k]);
                
        System.out.println(f[v][m]);
    }

}
7.背包问题求方案数

1.问题描述 如01背包问题方案数 值恰好为m的方案数

2.问题分析

f[0,0] = 1;

f[i,j] = f[i - 1,j] + f[i - 1,j - v[i]] 方案数等于不要第i个数的+要第i个数的

3.代码

8.求背包问题的具体方案

1.问题描述

2.问题分析:其实判断每个物品是否被选

具体方案 对应最短路

3.代码

9.有依赖的背包

1.问题描述:

有 N 个物品和一个容量是 V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MrcoDwe3-1666785707676)(https://www.acwing.com/media/article/image/2018/10/18/1_bb51ecbcd2-QQ%E5%9B%BE%E7%89%8720181018170337.png)]

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。

接下来有 N 行数据,每行数据表示一个物品。
第 行有三个整数 vi,wi,pi用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。

输出格式

输出一个整数,表示最大价值。

数据范围

1≤N,V≤100
1≤vi,wi≤100

父节点编号范围:

  • 内部结点:1≤pi≤N
  • 根节点 pi=−1

输入样例

5 7
2 3 -1
2 2 1
3 5 1
4 7 2
3 6 2

输出样例:

11

2.问题分析

显然root节点是肯定存在的 因为root是所有子节点的父节点 所以考虑从root节点递归

f[u,j]表示以u为父节点体积不超过j的最大价值。这里不以方案数来划分,以体积划分。(若以方案数:极端情况 99个子节点 需枚举299 若以体积量从0~100共101次)。

f[u,j]的第一维代表的是节点,对应树形DP问题;第二维是体积,代表一维分组背包问题

f[u,j]:在”以u为根节点的子树”中选,节点u必选,所选体积不超过j,的所有方案中,价值最大的方案的价值

计算f[u,j]时,先通过分组背包的方式计算在所有孩子节点中选的最大价值,最后再考虑节点u。设节点u有p个孩子节点,那么就相当于有p组物品。

物品组为1∼p,总共可用的体积为m−v[u]现在我们眼中只有p组物品,先不考虑父亲节点,只考虑计算这p组物品的最大价值

根据分组背包的做法,首先枚举物品组,也就是节点u的某个孩子son,对应代码的for (int i = h[u]; i != -1; i = ne[i])

其次枚举体积j,也就是考虑在1∼son的物品组中选,所选体积不超过jj的最大价值。
最后枚举在物品组son中选体积为k的物品,k∈[0,j],因为1∼son的物品组一共选了不超过j的体积

状态转移是f[u,j] = max(f[u,j], f[u,j - k] + f[son,k])。

f[u,j] 由于体积j是从大到小枚举,所以这里f[u,j-k]表示在1∼son−1的物品组中选,体积不超过j−k的方案的最大价值,这里省略了表示”在1∼son−1的物品组中选”的维度,相当于一维的分组背包。而f[u,j]的第一维代表的不是背包问题,而是树形DP,第二维代表的才是分组背包。所以这道题是树形DP和背包的综合。

f[son,k]: 由于状态转移之前已经递归调用了dfs(son),所以以son为根的子树已经计算好了。f[son,k]表示在以son为根的子树中选,所选体积不超过kk的最大价值
综上,f[u,j−k]+f[son,k]的前半部分更趋向于分组背包,后半部分趋向于树形DP

计算完f[u,* ]之后,f[u, *]代表的其实是在节点u的所有子树1∼p中选的最大价值,没有计算u的价值,所以需要加上最后的两个for循环

3.代码

import java.util.*;
public class Main{
    static int N = 110;
    static int n,m,idx;
    static int[][] f = new int[N][N];//表示第i个节点中选(i这个父节点以及他的所有子节点),总体积不超过j的方法的最大值
    static int[] v = new int[N];//体积
    static int[] w = new int[N];//价值
    static int[] h = new int[N],e = new int[N],ne = new int[N];
    public static void add(int a,int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }
    //u表示父节点
    public static void dfs(int u){
        for(int i = h[u];i != -1 ; i = ne[i]){
            //类似于分组背包 先遍历不同组 每个父节点下的子节点可看作一组 可以将头节点h[a]看作一组的标号
            int son = e[i];//遍历子节点
            dfs(son);
            //因为父节点是要保留的,所以我们的体积要预留出来放父节点的位置,d = m - v[u]
            //然后这里为什么从大到小枚举体积,类似01背包,这里被优化了一重循环,空间优化掉了,本来是三位的定义f[][][]
            for(int j = m - v[u]; j >= 0 ; j -- ){//循环体积
                for(int k = 0 ; k <= j ; k ++ ){ // 然后枚举决策,子节点只要不超过我们预留了父节点之后的体积d
                    //这一个节点的最大值,就等于预留了能够放父节点之后剩余的体积d里面,
                    //在子节点中挑能够满足不超过d 并且 选择不超过且最大的子节点体积
                    //f[u][j - k] + f[son][k] 这个的意思是加上这个f[son][k]儿子之后空间还有多少会不会超过了,
                    //但是我们已经限制了k比总体积小,所以永远不会超过,所以这个子节点一定是可以选择的
                    f[u][j] = Math.max(f[u][j],f[u][j - k] + f[son][k]);
                }
            }

        }
        //这个是我们的父节点,所以需要存下来的,体积要大于我们的父节点才能够存下来
        //f[u][i - v[u]] +  w[u] 这个的意思是上面dfs中我们预留了父节点位置剩余的d在这一节点中子节点能够选择的最大价值
        //然后再加上我们的父节点的价值,f[u][i]就是我们现在这一整个节点中的最大价值
        for(int i = m ; i >= v[u]; i -- ) f[u][i] = f[u][i - v[u]] +  w[u];
         //这个是不满足的情况,因为剩余的体积连放根节点都不够,所以这种情况不存在,直接赋值成0
        for(int i = 0 ; i < v[u] ; i ++ ) f[u][i] = 0;
    }
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();//n个节点
        m = scan.nextInt();//总体积
        Arrays.fill(h,-1);//初始化所有父节点
        int root = 0;
        for(int i = 1 ; i <= n ; i ++ ){
            v[i] = scan.nextInt();//体积
            w[i] = scan.nextInt();//价值
            int p = scan.nextInt();//依赖的父节点
            if(p == -1){
                root = i;//表示i这个节点是父节点
            }else{
                add(p,i);//表示p到i连一条有向边,p是父节点
            }
        }
        dfs(root);//从根节点开始递归

        //最后按照定义输出,根节点这一节点中选,总体积不超过m的最大价值
        System.out.println(f[root][m]);
    }
}

2.线性DP

1.数字三角形

1.问题描述:给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        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

2.问题分析

Dp
状态表示
状态计算
集合
属性:Max

先对该数字三角形进行二维定义



              1
1-----------7   2
2---------3   8   3
3-------8   1   0   4
4-----2   7   4   4   5
5---4   5   2   6   5   

对第i行计为i行1 对第j条斜线 计为第j列 对于状态表示:f[i] [j] 表示所有从起点到元素[i] [j]的最大路径。

其中f[i] [j]=Max{从左上角下来的路线,从右上角下来的路线}=Max{f[i-1] [j-1]+a[i] [j], f[i-1] [j]+a[i] [j]}

ps:小技巧 如果下标有i-1 那么最好从i=1开始 可以少一些判断 同时动态规划的时间复杂度计算一般为 T(状态数量* 转移数量)

3.代码

[法一]自顶向下 最后需求所有最低端的最大值

#include<iostream>
#include<algorithm> 
using namespace std;

const int N=510,INF=1e9;
int n,m;
int a[N][N];
int f[N][N];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			scanf("%d",&a[i][j]);
    //需将元素初始化为负无穷 由于数组元素默认为0 而路径上的元素可能为负
    //还有要将初始化时多一点 将i j从0开始 还有将j的最大扩展至i+1
    //比如对于斜边上的元素他们的左上方或右上方是不存在元素的 如果不初始化可能和为0大于右上方或左上方路径之和负数
	for(int i=0;i<=n;i++)
		for(int j=0;j<=i+1;j++)
			f[i][j]=-INF;
	f[1][1]=a[1][1];
	for(int i=2;i<=n;i++)
		for(int j=1;j<=i;j++)
			f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
	int res=-INF;
	for(int i=1;i<=n;i++) res=max(res,f[n][i]);
	printf("%d",res);
	return 0;
}

[法二]自下向最顶层 最后汇聚至第一个元素

#include<iostream>
#include<algorithm> 
using namespace std;
const int N=510;
int n,m;
int a[N][N];
int f[N][N];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			scanf("%d",&a[i][j]);
	for(int j=1;j<=n;j++) f[n][j]=a[n][j]; 
	for(int i=n-1;i>=1;i--)
		for(int j=1;j<=i;j++)
			f[i][j]=max(f[i+1][j+1]+a[i][j],f[i+1][j]+a[i][j]);
	printf("%d",f[1][1]);
	return 0;
}

不用初始化 保证了每一个元素的左下和右下必有元素

2.最长上升子序列

1.问题描述

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000,−109≤数列中的数≤109

输入样例:

7
3 1 2 1 8 5 6
//挑1 2 5 6得到4

输出样例:

4

2.问题分析

Dp问题先考虑能用什么维度来表示状态 如果有是否可以简化维数 对于时间复杂度而言每简化一维就少一维

Dp
状态表示
状态计算
集合
属性:Max

这里可用一维来记录状态 f[i]表示所有以第i个数结尾的上升子序列的长度最大值(所以这里的定义已确定结尾一定为i)

这里已经假设f[i]=以a[i]结尾的数的上升子序列长度最大值=(if a[j]<=a[i] )Max(f[0],f[1],…f[j],…f[i-1])+1

3.代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n;
int a[N],f[N];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++){
        //先初始化f[i]即若前面的所有元素都不满足a[j]<a[i]
		f[i]=1;
        //利用循环不断更新f[i]
		for(int j=1;j<i;j++)
			if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
		
	}
    //利用循环找到以a[i]结尾的最大长度的上升子序列
	int res=0;
	for(int i=1;i<=n;i++) res=max(res,f[i]);
	printf("%d\n",res);
	return 0;
}

时间复杂度T(n2)

保存序列具体数据:

最长上升子序列II 数据范围为1≤N≤100000 会超时

贪心法求解

对于一个序列 认为一个数ai > aj (i > j)那么能接在ai后面的数一定能接在aj后面 (有些序列便不必存储下来)可以发现长度为1~k的上升子序列的末尾数一定是上升的 否则 必然会从长度更大的子序列中找到一个数填进长度更小的序列 于是进行替换

遍历每个数 然后二分求解到小于该数的最大值 O(nlogn)

import java.util.*;

public class Main{
    static int N = 100010,n;
    static int[] q = new int[N];//所有不同长度子序列末尾最小值的那个子序列的末尾数
    static int[] a = new int[N];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        for(int i = 0;i < n;i ++) a[i] = sc.nextInt();
        int len = 0;//q里的元素个数
        q[0] = (int)-2e9;
        for(int i = 0;i < n;i ++ )
        {
            int l = 0,r = len;//从len个子序列中找比a[i]小的最大子序列
            while(l < r)
            {
                int mid = l + r + 1 >> 1;
                if(q[mid] < a[i]) l = mid;
                else r = mid - 1;
            }
            len = Math.max(len,r + 1);//看是否会新增一列长度没出现过的子序列
            q[r + 1] = a[i];//并替换成末尾更小的一个数
            
        }
        
        System.out.println(len);
    }
}
3.最长公共子序列

1.问题描述

给定两个长度分别为 N和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数 N 和 M。

第二行包含一个长度为 N 的字符串,表示字符串 A。

第三行包含一个长度为 M 的字符串,表示字符串 B。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N,M≤1000

输入样例:

4 5
acbd
abedc
//abd即为所求

输出样例:

3

2.问题分析

集合可表示为 在第一个子序列前i个元素和第二个子序列前j个元素相同的最长子序列

集合划分:首先考虑第一个序列中的前i个字母中的最后一个a[i]和第二个序列中的前j个字母中的最后一个b[j] 二者有选或不选 故划分成三份。

1)对于第a[i]选和b[j]不选的情况 可表示为f[i, j-1](其实不是这样 还包括了不选第a[i]个 但是由于求最大值故可包括)

2)对于第a[i]不选和b[j]选的情况 可表示为f[i-1, j](其实不是这样 还包括了不选第b[j]个 但是由于求最大值故可包括)

3)对于a[i]和b[j]都选上的情况 可表示为f[i-1, j-1]+1(对于此情况来说a[i]和b[i]为匹配的子序列的最后一个数)

3.代码

import java.util.*;

public class Main{
    static int N = 1010,n,m;
    static char[] a = new char[N];
    static char[] b = new char[N];
    static int[][] f = new int[N][N];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        String s1 = " " + sc.next();
        String s2 = " " + sc.next();
        a = s1.toCharArray();
        b = s2.toCharArray();
        for(int i = 1;i <= n;i ++)
            for(int j = 1;j <= m;j ++)
            {
                f[i][j] = Math.max(f[i - 1][j],f[i][j - 1]);
                if(a[i] == b[j]) f[i][j] = Math.max(f[i][j],f[i - 1][j - 1] + 1);
            }
        
        System.out.println(f[n][m]);
    }
}
4.最短编辑距离

1.问题描述:给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A 的某个位置插入某个字符。
  3. 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

输入格式

第一行包含整数 n,表示字符串 A 的长度。

第二行包含一个长度为 n 的字符串 A。

第三行包含整数 m,表示字符串 B 的长度。

第四行包含一个长度为 m 的字符串 B。

字符串中均只包含大小写字母。

输出格式

输出一个整数,表示最少操作次数。

数据范围

1≤n,m≤1000

输入样例:

10 
AGTCTGACGC
11 
AGTAAGTAGGC

输出样例:

4

2.思路:考虑最后一次插入、删除或修改。f(i,j)表示 A序列前i个数的操作f[i] [j]次之后和B序列的前j号数相等

每次操作为只操作第i号数 插入f[i] [j - 1] + 1、删除f[i - 1] [j] + 1、修改 f[i - 1] [j - 1] + 1/0(如果相等可以不修改则加0)

import java.util.*;

public class Main{
    static int N = 1010,n,m;
    static char[] a = new char[N];
    static char[] b = new char[N];
    static int[][] f = new int[N][N];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        String str1 = " " + sc.next();    
        a = str1.toCharArray();
        m = sc.nextInt();
        String str2 = " " + sc.next();
        b = str2.toCharArray();
        //初始化 
        for(int i = 1;i <= n;i ++) f[i][0] = i;
        for(int i = 1;i <= m;i ++) f[0][i] = i;
        
        for(int i = 1;i <= n;i ++)
            for(int j = 1;j <= m;j ++)
            {
                f[i][j] = Math.min(f[i - 1][j],f[i][j - 1]) + 1;
                if(a[i] == b[j]) f[i][j] = Math.min(f[i][j],f[i - 1][j - 1]);
                else f[i][j] = Math.min(f[i][j],f[i - 1][j - 1] + 1);
            }
            System.out.println(f[n][m]);
    }
}

3.区间DP

涵盖四个方面:环形问题、二维区间DP、记录方案数、区间Dp+高精度

石子合并问题

一维形式

1.问题描述:设有 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

2.问题分析

如果暴力求解 枚举次数为(n - 1)!

状态表示f[i] [j]为从第i号数到第j号数的最小合并代价 划分为按第k个数将一堆数划分为两堆k = i,i + 1,…,j - 1

则f[i] [j] = f[i] [k] + f[k + 1] [j] + s[j] - s[i - 1](还要包括最后一次将这堆数合并起来的代价)

3.代码

import java.util.*;

public class Main{
    static int N = 310,n;
    static int[] a = new int[N];
    static int[] s = new int[N];
    static int[][] f = new int[N][N];
    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();
            s[i] = s[i - 1] + a[i];
        }
        for(int len = 2;len <= n;len ++)
            for(int i = 1;i + len - 1 <= n;i ++)
            {
                int l = i,r = i + len - 1;
                f[l][r] = 0x3f3f3f3f;//不能直接在最外层循环赋值 否则f[l][k],f[k + 1][r]为正无穷 实际当左右相等应为0
                for(int k = l;k < r;k ++)
                    f[l][r] = Math.min(f[l][r],f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
            }
        System.out.println(f[1][n]);
    }
}

环形形式

将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:

  • 选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
  • 选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。

输入格式

第一行包含整数 n,表示共有 n 堆石子。

第二行包含 n 个整数,分别表示每堆石子的数量。

输出格式

输出共两行:

第一行为合并得分总和最小值,

第二行为合并得分总和最大值。

数据范围

1≤n≤200

输入样例:

4
4 5 9 4

输出样例:

43
54

思路:考虑到是一个圆形 如果要合并成一堆至少在点与点之间连接n - 1条线 最后可以变成最后只有两个点没有连线

这里可以考虑枚举这个空挡n种 则有n * n3 超时 则考虑 把项链拉直比如有5个点 1、2、3、4、5

则变为1、2、3、4、5、1、2、3、4、5 则求值维度为(2n)3 此方法适用于所有环形问题

import java.util.*;

public class Main{
    static int N = 410,n;
    static int[] w = new int[N];
    static int[] s = new int[N];
    static int[][] f = new int[N][N];//代表左右端点
    static int[][] g = new int[N][N];
        public static void main(String[] args){
            Scanner sc = new Scanner(System.in);
            n = sc.nextInt();
            for(int i = 1;i <= n;i ++ )
            {
                w[i] = sc.nextInt();
                w[i + n] = w[i];//把环形拉成链
            }
            //处理出前缀和
            for(int i = 1;i <= 2 * n;i ++ ) s[i] = s[i - 1] + w[i];
            
            for(int i = 0;i < N;i ++ ) Arrays.fill(f[i],-0x3f3f3f3f);//求最大值
            for(int i = 0;i < N;i ++ ) Arrays.fill(g[i],0x3f3f3f3f);//求最小值
            
            //迭代式求解
            for(int len = 1;len <= n;len ++)
                for(int l = 1;l + len - 1 <= 2 * n;l ++)
                {
                    int r = l + len - 1;
                    if(len == 1) g[l][r] = f[l][r] = 0;
                    else{
                        //如果len == 1 无法进入下层循环 如果把赋值为0放进去没用
                    for(int k = l;k < l + len - 1;k ++)
                    {
                            f[l][r] = Math.max(f[l][r],f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
                            g[l][r] = Math.min(g[l][r],g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
                    }
                    }
                }
                    
            int minv = 0x3f3f3f3f,maxv = -0x3f3f3f3f;
            for(int i = 1;i <= n;i ++ )
            {
                maxv = Math.max(maxv,f[i][i + n - 1]);
                minv = Math.min(minv,g[i][i + n - 1]);
            }
            System.out.println(minv);
            System.out.println(maxv);
        }
        
    }

4.计数DP

5.数位统计

多是分类讨论计数 类比于 0~9 10~99 100~999

6.状态压缩DP

用集合或2进制表的形式表示状态(类比于状态机 状态机以时序的方式来表示状态)

分为两大类

  • 棋盘式(基于连通性)
  • 集合式(看是否在集合中)
蒙德里安的梦想

求把 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

思路:考虑先将横着的方块摆完 剩下的放竖着的 用0 1来表示每一列的状态 1表示这行有一块横放的方块

f[i,j]表示为第i列的状态为j的满足题意的数量 其中要求每一列的挨着的空白格的数量必须为偶数且前后两列不能有重叠的方块

代码:

import java.util.*;
//将横放的摆完 剩下的放竖着的
public class Main{
    static int N = 12,M = 1 << 11;
    static boolean[] st = new boolean[M];
    static long[][] f = new long[N][M];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        while(true){
            int n = sc.nextInt();
            int m = sc.nextInt();
            if(n == 0) break;
            for(int i = 0;i < 1 << n;i ++)
            {
                //开始枚举2^n个状态
                st[i] = true;
                int cnt = 0;
                for(int j = 0;j < n;j ++)
                    if(((i >> j) & 1) == 1)
                    {
                        if((cnt % 2) == 1) st[i] = false;
                        cnt = 0;
                    }
                    else cnt ++;//当前连续0的个数
                
                 if((cnt % 2) == 1) st[i] = false;
            }
            f[0][0] = (long)1;
            for(int i = 1;i <= m;i ++)
                for(int j = 0;j < (1 << n);j ++)
                    for(int k = 0;k < (1 << n);k ++)
                        //保证一列中间隔数为偶数 前后列没有重合
                        if((j & k) == 0 && st[j | k]) f[i][j] += f[i - 1][k];
                        
            System.out.println(f[m][0]);
            for(int i = 0;i < N;i ++ ) Arrays.fill(f[i],0);
        }
    }
}
最短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

思路:不同于最短路在于 不重不漏 且起点为0 终点为n - 1

如果考虑暴力枚举 需要20!来枚举路径

这里用f[i , j]表示走过了i的状态(如走过了第0,1,4则i = 10011)且正走到第j号点

枚举倒数第二个点是什么来分类k(k = 0、1、2…n - 1)f[i,j] = f[i - {j},k] + w(k,j)

代码

import java.util.*;

public class Main{
    static int N = 21,n;
    static int[][] g = new int[N][N];
    static int[][] f = new int[1 << N][N];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        for(int i = 0;i < n;i ++ )
            for(int j = 0;j < n;j ++ )
                g[i][j] = sc.nextInt();
        
        for(int i = 0;i < (1 << N);i ++) Arrays.fill(f[i],0x3f3f3f3f);
        f[1][0] = 0; //先把0号点放入集合
        for(int i = 0;i < (1 << n);i ++)
            for(int j = 0;j < n;j ++ )
                if(((i >> j) & 1) == 1)//首先保证j在i中
                    for(int k = 0;k < n;k ++)//枚举倒数第二个点k
                        if((((i - (1 << j)) >> k) & 1) == 1)//保证减去j后 k也在i中
                            f[i][j] = Math.min(f[i][j],f[i - (1 << j)][k] + g[k][j]);
                            
        System.out.println(f[(1 << n) - 1][n - 1]);     
    }   
}

7.树形DP

类似于状态机

没有上司的舞会

题目描述:Ural 大学有 NN 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

输入格式

第一行一个整数 N。

接下来 N 行,第 i 行表示 i号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。

输出格式

输出最大的快乐指数。

数据范围

1≤N≤6000 −128≤Hi≤127

输入样例:

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

输出样例:

5

思路:以f(u,0)、f(u,1)分别表示以u为根节点不取u和取u的最大化收益

f(u,0) = Max(f(t1,0),f(t1,1)) + Max(f(t2,0),f(t2,1))+…;f(u,1) = f(t1,0) + f(t2,0)+…;(t1、t2…等为u的所有子节点)

故先求出所有子节点的状态 时间复杂度为O(n)每个子节点只算一遍

public class Main{
    static int N = 60010,n,idx;
    static int[] happy = new int[N];
    static int[] h = new int[N];
    static int[] ne = new int[N];
    static int[] e = new int[N];
    static boolean[] has_father = new boolean[N];
    static int[][] f = new int[N][2];
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        for(int i = 1;i <= n;i ++)  happy[i] = sc.nextInt();
        //初始化h所有指针都指向-1
        Arrays.fill(h,-1);
        for(int i = 0;i < n - 1;i ++ )
        {
            int a = sc.nextInt();
            int b = sc.nextInt();
            has_father[a] = true;
            add(b,a);
        }
        //计算根节点 因为是有向图(有上司和下属的关系)
        int root = 1;
        while(has_father[root]) root ++;
        dfs(root);
        
        System.out.println(Math.max(f[root][1],f[root][0]));
    }
    private static void dfs(int u){
        f[u][1] = happy[u];
        
        for(int i = h[u];i != -1;i = ne[i]){
            int j = e[i];
            dfs(j);
            f[u][0] += Math.max(f[j][0],f[j][1]);
            f[u][1] += f[j][0];
        }
    }
    private static void add(int a,int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx ++;
    }
}

8.记忆化搜索

以递归的方式求解状态 一定不能存在环

9.状态机

简单定义:把不好直接表示的状态分为几个状态来表示并其之间能够转换

问题:大盗阿福

问题描述:

传统思路:f[ i ]前i个店铺的最大收益 f[ i ] = max(f[i - 2] + w[ i ],f[i - 1])

状态机:把每个f[ i ]分解为0(未选) 1(选择)两个状态

f[i ,0] = Math.max(f[i - 1 ,0],f[i - 1 ,1]);

f[i ,1] = f[ i - 1 ,0] + w[i];

6.贪心

对于贪心 一般没有套路和固定模板 可自己先尝试据部分例子 摸索出一套算法 最后证明即可

贪心问题 每次(每一步)只选择看眼前看起来最优的情况(当前最优解) 比较短视

总的来说 贪心问题最优解 必为单峰函数(最终会走到最大点) 若为多峰 可能只会走到其中一极值

最后证明算法正确性

1.区间选点

1.问题描述:

给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

输入格式

第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示所需的点的最小数量。

数据范围

1≤N≤105,
−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5

输出样例:

2

2.问题分析

//一般区间问题先排序 可按右端点从小到大排序 从前往后一次枚举每个区间

//尽可能选最右的点以便覆盖更多的区间

//如果当前区间中已经包含点,则直接pass 否则选择当前区间最右端点

//证明正确性 可行解=最优解

数学中常用判等手法 A<=B;A>=B==》A=B

最坏情况 每个区间两两都不相交 每个区间

还有一种情况????????????????????????????????

3.代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n;
//定义一个结构 包含左右端点
struct Range{
	int l,r;
	//重载小于号
	bool operator< (const Range &w)const
	{
		return r < w.r; 
	} 
}range[N];
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		range[i] = {l,r};
	}
    //按右端点排序
	sort(range,range+n);
    //res用来计数 ed为上一个点坐标 初值为负无穷
	int res=0,ed=-2e9;
	for(int i=0;i < n;i++) 
        //倘若上一个点没在该区间内则 res+1 否则continue
		if(range[i].l>ed){
			res++;
			ed=range[i].r;
		}
	cout<<res<<endl;
	return 0;
}
2.最大不相交区间数量

1.问题描述

给定 N 个闭区间 [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

输入格式

第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示可选取区间的最大数量。

数据范围

1≤N≤105,
−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5

输出样例:

2

2.问题分析

//第一题:一个点尽可能占更多空间 第二题:希望更多的空间不相交 意思一样(????????????????????

原理分析后同上

证明正确性

3.代码

7.时空复杂度分析

参考蓝桥杯课程第一讲的图片(规律总结)

一般是在107—108 可以通过 如果超过108 那么超时 同时可根据题目给出的范围 可大致判断出时间复杂度 从而大致判断出解题方法

各种经典算法的时间复杂度

8.常见错误措施

1.TLE(Time Limited Exeception)超时

2.Memory limit exceed 内存开辟过大

3.Segmentation fault 用删除代码法检查

9.算法小题总结

1.将某进制数转化为十(秦九韶算法)

思想:如101102 = ((((1x2)+0)x2+1)x2+1)x2+0 每取一位的操作同上吧
dfs(root);

    System.out.println(Math.max(f[root][1],f[root][0]));
}
private static void dfs(int u){
    f[u][1] = happy[u];
    
    for(int i = h[u];i != -1;i = ne[i]){
        int j = e[i];
        dfs(j);
        f[u][0] += Math.max(f[j][0],f[j][1]);
        f[u][1] += f[j][0];
    }
}
private static void add(int a,int b){
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}

}




### 8.记忆化搜索

以递归的方式求解状态 一定不能存在环

### 9.状态机

简单定义:把不好直接表示的状态分为几个状态来表示并其之间能够转换



问题:大盗阿福

问题描述:

传统思路:f[ i ]前i个店铺的最大收益 f[ i ] = max(f[i  - 2] + w[ i ],f[i - 1])

状态机:把每个f[ i ]分解为0(未选) 1(选择)两个状态

f[i ,0] = Math.max(f[i - 1 ,0],f[i - 1 ,1]);

f[i ,1] = f[ i - 1 ,0] + w[i];

## 6.贪心

对于贪心 一般没有套路和固定模板 可自己先尝试据部分例子 摸索出一套算法 最后证明即可

贪心问题 每次(每一步)只选择看眼前看起来最优的情况(当前最优解) 比较短视

总的来说 贪心问题最优解 必为单峰函数(最终会走到最大点) 若为多峰 可能只会走到其中一极值

最后证明算法正确性

#### 1.区间选点

1.问题描述:

给定 N 个闭区间 [a<sub>i</sub>,b<sub>i</sub>],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

输入格式

第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 a<sub>i</sub>,b<sub>i</sub>,表示一个区间的两个端点。

输出格式

输出一个整数,表示所需的点的最小数量。

数据范围

1≤N≤10<sup>5</sup>,
−10<sup>9</sup>≤a<sub>i</sub>≤b<sub>i</sub>≤10<sup>9</sup>

输入样例:

3
-1 1
2 4
3 5


输出样例:

2


2.问题分析

//一般区间问题先排序 可按右端点从小到大排序 从前往后一次枚举每个区间

//尽可能选最右的点以便覆盖更多的区间

//如果当前区间中已经包含点,则直接pass 否则选择当前区间最右端点

//证明正确性 可行解=最优解

数学中常用判等手法 A<=B;A>=B==》A=B

最坏情况 每个区间两两都不相交 每个区间

还有一种情况????????????????????????????????

3.代码

```c++
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n;
//定义一个结构 包含左右端点
struct Range{
	int l,r;
	//重载小于号
	bool operator< (const Range &w)const
	{
		return r < w.r; 
	} 
}range[N];
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		range[i] = {l,r};
	}
    //按右端点排序
	sort(range,range+n);
    //res用来计数 ed为上一个点坐标 初值为负无穷
	int res=0,ed=-2e9;
	for(int i=0;i < n;i++) 
        //倘若上一个点没在该区间内则 res+1 否则continue
		if(range[i].l>ed){
			res++;
			ed=range[i].r;
		}
	cout<<res<<endl;
	return 0;
}
2.最大不相交区间数量

1.问题描述

给定 N 个闭区间 [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

输入格式

第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示可选取区间的最大数量。

数据范围

1≤N≤105,
−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5

输出样例:

2

2.问题分析

//第一题:一个点尽可能占更多空间 第二题:希望更多的空间不相交 意思一样(????????????????????

原理分析后同上

证明正确性

3.代码

7.时空复杂度分析

参考蓝桥杯课程第一讲的图片(规律总结)

一般是在107—108 可以通过 如果超过108 那么超时 同时可根据题目给出的范围 可大致判断出时间复杂度 从而大致判断出解题方法

各种经典算法的时间复杂度

8.常见错误措施

1.TLE(Time Limited Exeception)超时

2.Memory limit exceed 内存开辟过大

3.Segmentation fault 用删除代码法检查

9.算法小题总结

1.将某进制数转化为十(秦九韶算法)

思想:如101102 = ((((1x2)+0)x2+1)x2+1)x2+0 每取一位的操作同上吧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值