23考研算法复习

  • 一.图论相关算法
  • 1.拓扑排序
  • 2.最小生成树
  • 2.1 Prim算法朴素实现
  • 2.2 最小生成树Kruskal实现
  • 3.最短路
  • 3.1朴素版Dijkstra
  • 3.2Bellman-ford
  • 3.3Floyd
  • 二.排序相关算法
  • 1.快速排序
  • 2.归并排序及应用(求逆序对)
  • 三.23考研个人准备&模板|例题
  • 1.链表题
  • 1.1链表合并(严书课后习题)
  • 1.2反转链表(剑指 Offer II 024. 反转链表)
  • 2.简单算法
  • 2.1进制转换(略)
  • 2.2日期问题板子
  • 3.简单DP
  • 3.1LIS+LCS
  • 3.2背包板子
  • 3.2.1 01背包
  • 3.2.2 完全背包
  • 3.2.3 多重背包
  • 3.3一些杂题
  • 3.3.1 Acwing 12. 背包问题求具体方案
  • 3.3.2 AcWing 11. 背包问题求最优方案数
  • 3.3.3 Acwing1023. 买书(完全背包方案数)
  • 3.3.4 AcWing 278. 数字组合(01背包方案数)
  • 3.3.5 AcWing 532. 货币系统(完全背包求极大线性无关组)
  • 4.图论应用
  • 4.1最短路计数
  • 4.2多权值最短路
  • 5.课本例题
  • 5.1汉诺塔

PS:佛系考研,个人复习用,请勿催更

一.图论相关算法

1.拓扑排序

拓扑序的获取用bfs时用队列实现,用dfs时用栈实现,且dfs时用栈实现时,获取的是逆拓扑序,因为我们dfs是从任意一点出发,然后先找到最终端的点(出度为零的点),然后再逆着把dfs路径上的其他点压入栈中。具体见代码。

//bfs 队列实现
#include <bits/stdc++.h>
using  namespace std;
const int N=1e6+5;
int n,m;
int d[N],q[N];

struct Node{
    int id;
    Node* next;
    Node (int _id):id(_id),next(NULL){};
}*head[N];

void add(int a,int b){
    auto p=new Node(b);
    p->next=head[a];
    head[a]=p;
}

bool topsort(){
    int hh=0,tt=-1;

    for(int i=1;i<=n;i++){
        if(!d[i])q[++tt]=i;
    }

    while(hh<=tt){
        int t=q[hh++];
        for(auto p=head[t];p;p=p->next){
            if(--d[p->id]==0)q[++tt]=p->id;
        }
    }
    return tt==n-1;
}

main(){
    cin>>n>>m;
    while(m--){
        int a,b;
        cin>>a>>b;
        add(a,b);
        d[b]++;
    }

    if(!topsort())puts("-1");
    else{
        for(int i=0;i<n;i++)cout<<q[i]<<" ";
        cout<<endl;
    }
}
//dfs 栈实现
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+6;
int n,m;
int st[N],q[N],top;

struct Node{
    int id;
    Node* next;
    Node(int _id):id(_id),next(NULL){}
}*head[N];

void add(int a,int b){
    auto p= new Node(b);
    p->next=head[a];
    head[a]=p;
}

bool dfs(int u){
    st[u]=1;
    for(auto p=head[u];p;p=p->next){
        int v=p->id;
        if(!st[v]&&!dfs(v))return false;
        else if(st[v]==1) return false;
    }
    q[top++]=u;
    st[u]=2;
    return true;
}

bool topsort(){
    for(int i=1;i<=n;i++){
        if(!st[i]&&!dfs(i))return false;
    }
    return true;
}

main(){
    cin>>n>>m;
    while(m--){
        int a,b;
        cin>>a>>b;
        add(a,b);
    }

    if(!topsort())puts("-1");
    else{
        for(int i=n-1;i>=0;i--)cout<<q[i]<<" ";
        cout<<endl;
    }
}

2.最小生成树

2.1 Prim算法朴素实现

算法思想:每次从当前点出发,选择未在当前树的集合内的所有出边中最小的边,然后下次从该边相连的点继续进行该操作。若有n个点,则n-1一次后算法结束,即可得到最小生成树。
算法证明:反证法。(略)

#include <bits/stdc++.h>
using namespace std;
const int N=1005,INF=0x3f3f3f3f;
int n,m;
int g[N][N],dist[N];
bool st[N];

int prim(){
    memset(dist,0x3f,sizeof dist);
    int res=0;
    
    for(int i=0;i<n;i++){
        int t=-1;
        for(int j=1;j<=n;j++)
        if(!st[j]&&(t==-1||dist[t]>dist[j]))t=j;
        
        if(i&&dist[t]==INF)return INF;
        
        if(i)res+=dist[t];
        
        st[t]=true;
        
        for(int j=1;j<=n;j++)if(!st[j])dist[j]=min(dist[j],g[t][j]);
    }
    
    return res;
}

main(){
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=g[b][a]=min(g[a][b],c);
    }
    
    int res=prim();
    
    if(res==INF)puts("impossible");
    else cout<<res<<endl;
}
2.2 最小生成树Kruskal实现

算法思想:按边权从小到大一次选取,只要该边不在现有的树中不会形成环(用并查集实现判断),即可加入我们的生成树。

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m;
int p[N];
struct node{
    int a,b,c;
    bool operator<(const node &t)const{
        return c<t.c;
    }
}e[N];

int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

main(){
    cin>>n>>m;
    
    for(int i=0;i<m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        e[i]={a,b,c};
    }
    
    sort(e,e+m);
    
    for(int i=0;i<=n;i++)p[i]=i;
    
    int res=0,cnt=0;
    for(int i=0;i<m;i++){
        int a=find(e[i].a),b=find(e[i].b),c=e[i].c;
        if(a!=b){
            res+=c;
            p[a]=b;
            cnt++;
        }
    }
    
    if(cnt!=n-1)puts("impossible");
    else cout<<res<<endl;
}

3.最短路

最短路包括单源最短路(起点唯一),多源汇最短路(可以看成终点唯一,起点有多个)两种
1.单源最短路算法又可以分为不含负权边含负权边的最短路算法。
    可以解决不含负权边的算法主要是Dijkstra
    可以解决含负权边的算法主要是Bellman-ford
2.多元汇最短路主要是Floyd

补充:我们上面说的Dijkstra不能解决含负权边的最短路问题是指不能解决一般的含负权边的最短路问题(就是什么情况都可能发生,可能有负环)。
Dijkstra所谓的不能解决负权边的最短路问题,实质是无法解决含负环问题的最短路,因为Dijkstra本质是贪心算法,含负环的最短路会可能会使我们从当前点出发先走到了一条下一条看似很短(负)的道路,但是另一条边所含的道路中可能含有更短(负)的道路(这里说的很抽象,建议自己画个图理解理解)。

3.1朴素版Dijkstra

算法思想:类似Prim算法。最初所有除了起点以外的点全部置为无穷,每次从当前点出发,从当前已有的最短路集合向外更新其他点到起点的距离,然后选取当前最短路集合中距离起点距离最小的点,继续上述算法流程。

#include <bits/stdc++.h>
using namespace std;
const int N=2005,INF=0x3f3f3f3f;
int n,m;
int g[N][N],dist[N];
bool st[N];

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=0;i<n;i++){
        int t=-1;
        for(int j=1;j<=n;j++)
        if(!st[j]&&(t==-1||dist[t]>dist[j]))t=j;
        
        st[t]=true;
        
        for(int j=1;j<=n;j++)dist[j]=min(dist[j],dist[t]+g[t][j]);
    }
    return dist[n];
}

main(){
    cin>>n>>m;
    
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);
    }
    
    int res=dijkstra();
    
    if(res==INF)puts("-1");
    else cout<<res<<endl;
}
3.2Bellman-ford

PS:以前比赛都是无脑spfa,所以除了当初第一次并没有怎么写过Bellman-ford,但是考纲上有这个知识点,所以这里着重总结一下。
1.同spfa,Bellman-ford可以找负环,但是时间复杂度比spfa要高
2.Bellman-ford时间复杂度为O(nm)
3.Bellman-ford的算法过程类似DP,进行n次迭代,,每次循环所有边,每次循环跟新所有边(松弛操作dist[b]=min(dist[b],dist[a]+w[a][b]))。
4.第k次迭代其实求从起点出发,经过不超过k条边到达该点的最短路。所以Bellman-ford可以解决有边数限制的最短路(这个好像是比赛里面唯一会用Bellman的情景,hh)

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
struct node{
	int u,v,w;
}e[maxn];
int n,m,k,dis[maxn],backup[maxn];
int bellman_ford(){
	memset(dis,0x3f3f3f3f,sizeof(dis));
	dis[1]=0;
	for(int i=1;i<=k;i++){
		/**
			这里之所以要备份(用上一层状态去更新下一状态最短路)是因为直接用当前状态更新,会发生串联。
			比如我直接从最开始直接更新1->2然后2的点更新后又可以更新呢其他点,这样第一次迭代其实就相当于可以更新经过多条边的最短路而不是一条边。
			同理,我们如果使用上一层状态更新,只有1是可以更新其他点的,就算2在这一层被更新了,但是上一层的2是不能更新其他相邻点的。
			这样就保证了在用上一层的状态更新当前层状态时,我们只会最多比上一层的状态向外扩展一层(从至多经过k-1边到至多经过k条边的最短路)
		*/
		memcpy(backup,dis,sizeof(dis));
		for(int j=1;j<=m;j++){
			int a=e[j].u,b=e[j].v,c=e[j].w;
			if(dis[b]>backup[a]+c){
				dis[b]=backup[a]+c;
			}
		}
	}
	if(dis[n]>0x3f3f3f3f/2)return -1;
	return dis[n];
}
int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++)
	cin>>e[i].u>>e[i].v>>e[i].w;
	int t=bellman_ford();
	if(t==-1)cout<<"impossible"<<endl;
	else cout<<t<<endl;
}
3.3Floyd

算法思想:DP

void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

二.排序相关算法

1.快速排序

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n;
int a[N];

void qsort(int a[],int l,int r){
	if(l>=r)return;
	
	int i=l-1,j=r+1,x=a[(l+r)/2];
	while(i<j){
		do i++;while(x>a[i]);
		do j--;while(x<a[j]);
		if(i<j)swap(a[i],a[j]);
	}
	
	qsort(a,l,j);
	qsort(a,j+1,r);
}

main(){
	cin>>n;
	for(int i=0;i<n;i++)cin>>a[i];
	
	qsort(a,0,n-1);
	
	for(int i=0;i<n;i++)cout<<a[i]<<" ";
	cout<<endl;
}

2.归并排序及应用(求逆序对)

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n;
int a[N],tmp[N];

void merge_sort(int a[],int l,int r){
	if(l>=r) return;
	
	int mid=l+r>>1;
	merge_sort(a,l,mid);
	merge_sort(a,mid+1,r);
	
	int i=l,j=mid+1,k=0;
	while(i<=mid&&j<=r){
		if(a[i]<=a[j])tmp[k++]=a[i++];
		else tmp[k++]=a[j++];
	}
	
	while(i<=mid) tmp[k++]=a[i++];
	while(j<=r) tmp[k++]=a[j++];
	
	for(int i=l,j=0;i<=r&&j<=k;i++,j++)a[i]=tmp[j];
}

main(){
	cin>>n;
	for(int i=0;i<n;i++)cin>>a[i];
	
	merge_sort(a,0,n-1);
	
	for(int i=0;i<n;i++)cout<<a[i]<<" ";
	cout<<endl;
}

2.1归并排序求逆序对

1.这个方法915考过代码题,其实树状数组也行,时间复杂度都是O(nlogn)
2.关于代码中的res+=mid-i+1:其实本质就是每次归并时统计逆序对,因为这里的归并排序时二路归并,归并的两个子数组都是有序的所以一旦出现了逆序对(即a[i]>a[j]),后面的对于第一个数组的所有数都会与第二个数组的a[j]构成逆序对,所以有res+=mid-i+1;

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n;
int a[N],tmp[N];
int res=0;
void merge_sort(int a[],int l,int r){
	if(l>=r) return;
	
	int mid=l+r>>1;
	merge_sort(a,l,mid);
	merge_sort(a,mid+1,r);
	
	int i=l,j=mid+1,k=0;
	while(i<=mid&&j<=r){
		if(a[i]<=a[j])tmp[k++]=a[i++];
		else tmp[k++]=a[j++],res+=mid-i+1;
	}
	
	while(i<=mid) tmp[k++]=a[i++];
	while(j<=r) tmp[k++]=a[j++];
	
	for(int i=l,j=0;i<=r&&j<=k;i++,j++)a[i]=tmp[j];
}

main(){
	cin>>n;
	for(int i=0;i<n;i++)cin>>a[i];
	
	merge_sort(a,0,n-1);
	
	cout<<res<<endl;
}

三.23考研个人准备&模板|例题

1.链表题

1.1链表合并(严书课后习题)

这题有两种实现方式,遍历和递归都可以实现,具体见代码

//遍历实现
   ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode *p = new ListNode(-1);

        ListNode *head=p;

        while(l1&&l2){
            if(l1->val<l2->val){
                p->next=l1;
                l1=l1->next;
            }
            else{
                p->next=l2;
                l2=l2->next;
            }
            p=p->next;
        }

        if(l1)p->next=l1;
        if(l2)p->next=l2;

        return head->next;
    }

//递归实现
       ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if(l1==NULL) return l2;
        if(l2==NULL) return l1;
        
        if(l1->val<l2->val){
            l1->next=mergeTwoLists(l1->next,l2);
            return l1;
        }
        else{
            l2->next=mergeTwoLists(l1,l2->next);
            return l2;
        }

        return NULL;
    }
1.2反转链表(剑指 Offer II 024. 反转链表)
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *prev=nullptr;
        ListNode *curr=head;
        //ListNode *nx=curr->next;

        while(curr){
            ListNode *nx=curr->next;
            curr->next=prev;
            prev=curr;
            curr=nx;
        }

        return prev;
    }
};

2.简单算法

2.1进制转换(略)
2.2日期问题板子

注:闰年要么是400的整数倍,要么是4的整数倍且不是100的整数倍

//(Acwing 3218. 日期计算)
#include <bits/stdc++.h>
using namespace std;
int months[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};

int is_leap(int year){
	if(year%400==0||(year%4==0&&year%100)) return 1;
	return 0;
}

int get_day(int year,int month){
	if(month==2) return months[2]+is_leap(year);
		
	return months[month];
}

main(){
	int year,days;
	cin>>year>>days;
	
	int cnt=0;
	for(int month=1;month<=12;month++){
		for(int day=1;day<=get_day(year,month);day++){
			cnt++;
			if(cnt==days){
				cout<<month<<endl;
				cout<<day<<endl;
				return 0;
			}
		}
	}
}

3.简单DP

3.1LIS+LCS
//最长上升子序列
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,a[N],dp[N];

main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],dp[i]=1;
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<i;j++){
            if(a[i]>a[j])dp[i]=max(dp[i],dp[j]+1);
        }
    }
    
    int res=1;
    for(int i=1;i<=n;i++)res=max(res,dp[i]);
    
    cout<<res<<endl;
}

//最长公共子序列
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m,dp[N][N];
char a[N],b[N];

main(){
    cin>>n>>m;
    scanf("%s%s",a+1,b+1);
    
    int res=0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i]==b[j])dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
            else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            
            res=max(dp[i][j],res);
        }
    }
    
    cout<<res<<endl;
}
3.2背包板子

基础板子:01背包,完全背包,多重背包(本质还是01)
板子扩展:背包方案数,背包输出方案

3.2.1 01背包

状态表示:dp[i][j]表示从前j个物品中选,且当前背包体积不超过j的价值
状态转移:根据当前物品选与不选,将当前状态分为两个集合。若不可以选,则状态直接从上一层转移,若可以选当前物品,则分为选与不选转移当前状态

//朴素版
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int dp[N][N];

main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int v,w;
		cin>>v>>w;
		for(int j=0;j<=m;j++){
			if(j>=v)dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w);
			else dp[i][j]=dp[i-1][j];
		}
	}
	
    cout<<dp[n][m]<<endl;
}

//优化版
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int dp[N];

main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int v,w;
		cin>>v>>w;
		for(int j=m;j>=v;j--)
		dp[j]=max(dp[j],dp[j-v]+w);
	}
	
    cout<<dp[m]<<endl;
}
3.2.2 完全背包

一个物品可以选任意。其实跟01背包一样,将一个物品按取1~k次继续将状态集合划分即可

//朴素版
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int dp[N][N];

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

//优化版
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int dp[N];

main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int v,w;
		cin>>v>>w;
		for(int j=v;j<=m;j++){
			dp[j]=max(dp[j],dp[j-v]+w);
		}
	}
	
	cout<<dp[m]<<endl;
}
3.2.3 多重背包

本质就是拆成若干个01背包,朴素版O(N3),可以二进制优化为O(N2logN)
二进制优化思路:即按二进制的每一位才成若干个独立的物品,当成01背包来做,对于二进制的每一位的1要么选要么不选,这样就可以凑出来一个物品所有可取的数量。

//朴素版
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int dp[N];

main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int v,w,s;
		cin>>v>>w>>s;
		for(int k=1;k<=s;k++)
		for(int j=m;j>=v;j--){
			dp[j]=max(dp[j],dp[j-v]+w);
		}
	}
	
	cout<<dp[m]<<endl;
}
//二进制优化版
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m,cnt;
int v[N],w[N],dp[N];

main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		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;
			s=0;
		}
	}
	
	for(int i=1;i<=cnt;i++)
	for(int j=m;j>=v[i];j--){
		dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
	}
	
	cout<<dp[m]<<endl;
}
3.3一些杂题
3.3.1 Acwing 12. 背包问题求具体方案

类似最短路输出方案,记当前容量为k,每次倒退相应地改变容量即可

//Acwing 12. 背包问题求具体方案
#include <bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int v[N],w[N],dp[N][N];

main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
	
	for(int i=n;i>=1;i--){
		for(int j=0;j<=m;j++){
			if(j>=v[i])dp[i][j]=max(dp[i+1][j],dp[i+1][j-v[i]]+w[i]);
			else dp[i][j]=dp[i+1][j];
		}
	}
	
	vector<int>q;
	
	int k=m;
	for(int i=1;i<=n;i++){
		if(k>=v[i]&&dp[i][k]==dp[i+1][k-v[i]]+w[i]){
			q.push_back(i);
			k-=v[i];
		}
	}
	
	for(auto x:q)cout<<x<<" ";
	cout<<endl; 
}
3.3.2 AcWing 11. 背包问题求最优方案数

类似于求最短路方案数,f[i]表示背包体积不超过i时的最大价值,g[i]表示背包体积不超过i时价值最大的方案数(这里分析的是最优方案(最大价值)的方案数
1.记当前转移来的最大价值为k,k=max(f[j],f[j-v]+w),方案数为cnt
2.如果k从f[j]转移 则cnt+=g[j];
3.如果k从f[j-v]+w转移 则cnt+=g[j-v];

//AcWing 11. 背包问题求方案数  
#include <bits/stdc++.h>
using namespace std;
const int N=1005,mod=1e9+7;
int n,m;
int v[N],w[N];
int f[N],g[N];

main(){
	cin>>n>>m;
	
	g[0]=1;
	for(int i=n;i>=1;i--){
		int v,w;
		cin>>v>>w;
		for(int j=m;j>=v;j--){
			int k=max(f[j],f[j-v]+w);
			int cnt=0;
			
			if(k==f[j-v]+w)cnt=(cnt+g[j-v])%mod;
			if(k==f[j]) cnt=(cnt+g[j])%mod;
			
			g[j]=cnt,f[j]=k;
		}
	}
	
	int res=0;
	for(int i=0;i<=m;i++){
		if(f[m]==f[i])res=(res+g[i])%mod;
	}
	
	cout<<res<<endl;
}
3.3.3 Acwing1023. 买书(完全背包方案数)

题目描述:小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元。问小明有多少种买书方案?(每种书可购买多本)
PS:这个似乎时我考的自命题某年的最后一题,hhh。

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int a[]={10,20,50,100};
int n,dp[N];

main(){
    cin>>n;
    dp[0]=1;
    for(int i=0;i<4;i++){
        for(int j=a[i];j<=n;j++)
        dp[j]+=dp[j-a[i]];
    }
    cout<<dp[n]<<endl;
}
3.3.4 AcWing 278. 数字组合(01背包方案数)

题目描述:给定 N 个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。
PS:第二层枚举体积时,必须从大到小去枚举,这样才能保证不重不漏。

#include <bits/stdc++.h>
using namespace std;
const int N=2005;
int n,m;
int a[N],dp[N];

main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	
	dp[0]=1;
	for(int i=1;i<=n;i++){
		for(int j=m;j>=a[i];j--){
			dp[j]+=dp[j-a[i]];
		}
	}
	
	cout<<dp[m]<<endl;
}
3.3.5 AcWing 532. 货币系统(完全背包求极大线性无关组)

题目大意:给出n组数,将其看作长度为1的向量,求出极大线性无关组
思路:本题其实就是在说要求将这些数字划分为互相之间不能相互表示的组的数量。采用完全背包,将所有数从小到大排列,然后任意取某些数,凑成一个数,如果当前这个数不能被比之前小的数凑出,则他就可以单独再分出一个于之前所有向量组都无关的一个向量。
PS:具体见代码。

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m;
int a[N],dp[N];

void solve(){
	cin>>n,m=0;
	for(int i=0;i<n;i++)cin>>a[i],m=max(m,a[i]);
	
	sort(a,a+n);
	
	memset(dp,0,sizeof dp);
	dp[0]=1;
	
	int res=0;
	for(int i=0;i<n;i++){
		int x=a[i];
		if(dp[x])continue;
		res++;
		for(int j=x;j<=m;j++){
			dp[j]|=dp[j-x];
		}
	}
	
	cout<<res<<endl;
}

main(){
	int T;
	cin>>T;
	while(T--)solve();	
}

4.图论应用

4.1最短路计数

Acwng 1134. 最短路计数 有这个原题,但是我这里写的是朴素版的dijkstra,要过这个题改成堆优化的dij就行了。(此题是915 22考研的一道题目,要求手写dijkstra伪代码)

int dijkstra(){
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	cnt[1]=1;
	for(int i=0;i<n;i++){
		int t=-1;
		for(int j=1;j<=n;j++){
			if(!st[j]&&(t==-1||dist[t]>dist[j]))t=j;
		}
		
		st[t]=true;
		
		for(int j=1;j<=n;j++){
			//dist[j]=min(dist[j],dist[t]+g[t][j]);
			if(dist[j]>dist[t]+g[t][j]){
				cnt[j]=cnt[t];
				dist[j]=dist[t]+g[t][j];	
			}
			else if(dist[j]==dist[t]+g[t][j])cnt[j]+=cnt[t];
		}
		
	}
	return dist[n];
}
4.2多权值最短路

这是一类经典问题,比如求最短边数的最短路,其实跟上面那个最短路计数一样,在转移最短路时,动态维护c一个记录cnt[i]数组即可。(这两个题本质上都是DP)

int dijkstra(){
	memset(dist,0x3f,sizeof dist);
	for(int i=0;i<n;i++){
		int t=-1;
		for(int j=1;j<=n;j++){
			if(!st[j]&&(t==-1||dist[t]>dist[j]))t=j;
		}
		
		st[t]=true;
		
		for(int j=1;j<=n;j++){
			if(dist[j]==dist[t]+g[t][j]){
				cnt[j]=min(cnt[j],cnt[t]+1);
			}
			else if(dist[j]>dist[t]+g[t][j]){
				cnt[j]=cnt[t]+1;
				dist[j]=dist[t]+g[t][j];
			}
		}
	}
	return dist[n];
}

5.课本例题

5.1汉诺塔

定义 :Hanoi(n, a, b, c):将n个盘子从a柱移到c柱,b柱起中转作用。
f(n):n个盘子的汉诺塔的总移动次数。

Hanoi(n, a, b, c) = Hanoi(n-1, a, c, b) + 1 + Hanoi(n-1, b, a,
c):将a上面n-1个盘子移到b,再将a最下面的盘子移到c,再将b上的n-1个盘子移到c,此时,n盘子汉诺塔问题 变成了 移动一个盘子 +
两个n-1盘子汉诺塔问题。 故 f(n) = f(n-1) + 1 + f(n-1) = 2f(n-1) + 1
=> f(n) + 1 = 2(f(n-1) + 1)
=> f(n) + 1 = 2^(n-1) (f(1) + 1)
=> f(n) + 1 = 2^n
=> f(n) = 2^n -1
PS:汉诺塔问题来源牛客的某个题下面的评论,侵删。