算法学习记录:动态规划

前言:

  算法学习记录不是算法介绍,本文记录的是从零开始的学习过程(见到的例题,代码的理解……),所有内容按学习顺序更新,而且不保证正确,如有错误,请帮助指出。

学习工具:蓝桥OJ,LeetCode

背景知识:

你有动态规划相关基础知识。

算法学习记录:动态规划基础

目录

前言:

背景知识:

 正文:

模型一:背包问题

01背包:

蓝桥OJ 1174:小明的背包1

蓝桥OJ 2223:背包与魔法

蓝桥OJ 3741:倒水

蓝桥OJ 3637:盗墓分赃

蓝桥OJ 2945:蓝桥课程抢购

完全背包:

 蓝桥OJ 1175:小明的背包2

多重背包:

蓝桥OJ 389:摆花

 蓝桥OJ 4059:新一的宝藏搜寻加强版

单调队列优化多重背包:

二维费用背包:

 蓝桥OJ 3937:小蓝的神秘行囊

 分组背包:

 蓝桥OJ 1178:小明的背包5

模型二:树型DP

模型三:区间DP

模型四:状压DP

模型五:数位DP

模型六:期望DP


 正文:

动态规划:Dynamic Programing 。以下简称“DP”。

按方法分类:搜索法(DFS),迭代法

按实现方式分类:一维DP,二维DP

动态规划涉及的问题种类繁多,按照题目模型分类:

模型一:背包问题

01背包:

问题描述:

  有一个体积为V的背包,商店有n个物品,每个物品有一个价值v和体积w,每个物品只能够被拿一次,文能够装下物品的最大价值。

设状态dp[i][j]表示到第i个物品为止,拿的物品总体积为j的情况下的最大价值。

状态转移方程:

蓝桥OJ 1174:小明的背包1
#include<bits/stdc++.h>
using namespace std;

using ll = long long;
const int N = 105,M = 1010;
ll dp[N][M];

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

 观察发现:这题我们只关心第n个物品的情况,

所以没必要用二维数组把原来的所有情况存下

每次更新只用上一次数据,如果能直接进行覆盖,就采用一维数组解决问题。

优化:

dp[i][j]=dp[i-1][j],相当于dp[i-1]复制给dp[i],

将第一维优化掉直接当作一个数组

每次更新时,从后往前更新,此时dp[j]表示此时物品总体积为j时的物品最大价值

得到状态转移方程:

#include<bits/stdc++.h>
using namespace std;

using ll = long long;
const int N = 105,M = 1010;
ll dp[M];

int main()
{
	int n,V;cin >> n >> V;
	
	for(int i = 1;i <= n;i ++)
	{
		ll w,v;cin >> w >> v;
		for(int j = V;j >= w;j ++)
		{
		  dp[j] = max(dp[j],dp[j - w] + v);
		}
		
	}
	
	cout << dp[V] << endl;
	
	return 0;
}
蓝桥OJ 2223:背包与魔法

对每个物品有3种选择:不选、选但不用魔法、选且用魔法

状态转移方程:

#include<iostream>
using namespace std;
using ll = long long;
const int N = 1e4 + 9;
ll dp[N][2];

int main()
{
	int n,m,k;cin >> n >> m >> k;
	for(int i = 1;i <= n;i ++)
	{
		ll w,v;cin >> w >> v;
		for(int j = m;j >= 0;j --)
		{
			if(j >= w)
			{
				dp[j][0] = max(dp[j][0],dp[j - w][0] + v);
				dp[j][1] = max(dp[j][1],dp[j - w][1] + v);
			}
			if(j >= w + k)
			{
				dp[j][1] = max(dp[j][1],dp[j - w - k][0] + 2 * v);
			}
		}	
	}
	cout << max(dp[m][0],dp[m][1]) << endl;
	return 0;
}
蓝桥OJ 3741:倒水
//为了尽可能节约水,本题只有3种倒水方式:
//1、给当前客人倒水a毫升,使得总好感度增加b
//2、给当前客人倒水c毫升(c>a),使得总好感度增加d
//3、不给客人倒水(对应题目中的倒水小于a毫升,倒与不倒是一样的,还不如不倒),总好感度增加e 
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

ll dp[1005][1005];//dp[i][j]表示只考虑前i个客人,共倒水j毫升所得的最大好感度 
//易知对于第i个客人,给其倒水所得好感度的多少只与前i-1个客人有关 

int main()
{
    int N,M;
    cin>>N>>M;
    for(int i=1;i<=N;i++)//分别考虑前1~N个客人 
    {
        ll a,b,c,d,e;
        cin>>a>>b>>c>>d>>e;
        for(int j=0;j<=M;j++)//对第i个客人,分别考虑共倒0~M毫升水 
        {
            //若当前拥有的水小于a,干脆不倒水,好感度为前i-1个客人共倒j升水所得好感度 加上e 
            if(j<a)dp[i][j]=dp[i-1][j]+e;
            //若当前拥有的水不小于a但是小于c,则可以选择倒a毫升或者不倒水
            //若倒a毫升水,则好感度为 前i-1个客人共倒j-a毫升水所得好感度 加上b
            //若不倒水,则好感度为 前i-1个客人共倒j毫升水所得好感度 加上e
            //二者取较大值即可 
            else if(j>=a&&j<c)dp[i][j]=max(dp[i-1][j-a]+b,dp[i-1][j]+e);
            //若当前拥有的水足够多,依次考虑三种情况,同理可得 
            else dp[i][j]=max(dp[i-1][j-a]+b,max(dp[i-1][j-c]+d,dp[i-1][j]+e));
        }
    }
    cout<<dp[N][M]<<endl;//考虑前N个客人,共倒M毫升水所得好感度的最大值 即为最终答案 
    return 0;
}

优化:


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[1005];
int main()
{
	int N,M;
	cin>>N>>M;
	for(int i=1;i<=N;i++)
	{
		ll a,b,c,d,e;
		cin>>a>>b>>c>>d>>e;
		for(int j=M;j>=0;j--)
		{
			if(j<a)dp[j]=dp[j]+e;
			else if(j>=a&&j<c)dp[j]=max(dp[j-a]+b,dp[j]+e);
			else dp[j]=max(dp[j-a]+b,max(dp[j-c]+d,dp[j]+e));
		}
	}
	cout<<dp[M]<<endl;
	return 0;
}
蓝桥OJ 3637:盗墓分赃
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+5;
bool dp[N];
const int M=1e3+4;
int s[M];
int main()
{
	int n;cin>>n;
	int sum=0;
	for(int i=1;i<=n;i++)
	{
		cin>>s[i];
		sum+=s[i];
	}
	dp[0]=true;                //啥也不拿一定可以
	if(sum%2!=0)
		cout<<"no"<<'\n';
	else
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=sum;j>=1;j--)
			{
				if(j>=s[i])
					dp[j]=dp[j]||dp[j-s[i]];
			}
		}
		if(dp[sum/2])
			cout<<"yes"<<'\n';
		else
			cout<<"no"<<'\n';
	}
	return 0;
}
蓝桥OJ 2945:蓝桥课程抢购

由于要先考虑把时间短的放前面,使用结构体数组把数据关联起来,便于操作。

#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+4;
ll dp[55][N];            //表示在i个科目之前,j的等待时间下最大的价值
 struct Class
{
 int wait,j,value;
}c[55];
bool cmp(Class a,Class b)
{
   return a.j<b.j;
}
int main()
{
  ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
  int n;cin>>n;
  int sum=0;
  for(int i=1;i<=n;i++)
  {
    cin>>c[i].wait>>c[i].j>>c[i].value;
    sum=max(sum,c[i].j);
  }
  sort(c+1,c+n+1,cmp);               //先把截至时间短的放前面,贪心把时间短的先做了
  ll ans=0;
  for(int i=1;i<=n;i++)
  {
    for(int j=sum;j>=1;j--)
    {
      dp[i][j]=dp[i-1][j];               //先初始化成这个科目不选
      if(j>=c[i].wait&&j<=c[i].j)
      {
        dp[i][j]=max(dp[i][j],dp[i-1][j-c[i].wait]+c[i].value);
        ans=max(ans,dp[i][j]);
      }
    }
  }
  cout<<ans<<'\n';
  return 0;
}

优化:

#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+4;
ll dp[N];            //表示在i个科目之前,j的等待时间下最大的价值
struct Class
{
	int wait,j,value;
}c[55];
bool cmp(Class a,Class b)
{
	return a.j<b.j;
}
int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int n;cin>>n;
	int sum=0;
	for(int i=1;i<=n;i++)
	{
		cin>>c[i].wait>>c[i].j>>c[i].value;
		sum=max(sum,c[i].j);
	}
	sort(c+1,c+n+1,cmp);               //先把截至时间短的放前面,贪心把时间短的先做了
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=sum;j>=1;j--)
		{
			dp[j]=dp[j];               //先初始化成这个科目不选
			if(j>=c[i].wait&&j<=c[i].j)
			{
				dp[j]=max(dp[j],dp[j-c[i].wait]+c[i].value);
				ans=max(ans,dp[j]);
			}
		}
	}
	cout<<ans<<'\n';
	return 0;
}

完全背包:

又名无穷背包,每种物品有无数个背包。

即每个物品可以被拿无数次,有无限多个。

设状态dp[i]表示拿的物品总体积为i的情况下的最大价值。

状态转移方程:

 因为新数据的产生必须有先后,现在就必须用”新数据“来更新“新数据”。

 蓝桥OJ 1175:小明的背包2
#include<iostream>
using namespace std;
const int N = 1e3 + 9;
int dp[N];

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

对比这题与’小明的背包1‘,

可以发现:遍历顺序变化后,

dp[i]可以来自在相同t情况(遍历到了同一件物品)下刚刚被更新过的数据 。

这就计算了这件物品可以被用多次的情况。

多重背包:

有一个体积为V的背包,商店有n种物品,每种物品有一个价值v和体积w,每种物品有s个,问装下的最大价值。

只需在01背包模型的基础上再加一层循环,更新s次即可。

蓝桥OJ 389:摆花

对于每一个到达一个位置并种了某种花的情况,

方案数都是先种上一个位置并种了少用一种花的方案数,

具体来说:

这个方案数就是:保持种到相同位置,这最后一种花种了多少盆的所有情况的方案数之和

设状态dp[i][j]表示到第i种花为止(不一定以第i种花结尾),到第j个位置(1-j都放了花)的情况下的总方案数:

图解:

归纳出状态转移方程:

 

#include<bits/stdc++.h>
using namespace std;
const int N = 105;
using ll = long long;
const ll p = 1e6 + 7;
ll a[N],dp[N][N];
 
int main()
{
	int n,m; cin >> n >> m;
	for(int i = 1;i <= n; i ++)cin >> a[i];
	
	dp[0][0] = 1;
	
	for(int i = 1;i <= n;i ++)
	{
		for(int j = 0;j <= m;j ++)
		{
			for(int k = 0;k <= a[i] && k <= j; k ++){
				dp[i][j] = (dp[i][j] + dp[i - 1][j - k]) % p;
			}
		}
	}
	cout << dp[n][m] << endl;
	return 0;
}
 蓝桥OJ 4059:新一的宝藏搜寻加强版
#include<iostream>
using namespace std;
const int N = 205;
int dp[N];

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

 优化:原时间复杂度:O(n*s*V)

进行二进制优化:时间复杂度:O(n*logs*V)

#include<bits/stdc++.h>
using namespace std;

using ll = long long;
const int N = 1e3+7,M = 2e4 +7;


ll dp[M];

int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int n,m;cin >> n >> m;
	for(int i = 1;i <= n;i ++)
	{
		ll v,w,s;cin >> v >> w >> s;
		for(int k = 1;k <= s;s -= k,k +=k)
		{
			for(int j = m;j >= k * v;j--)dp[j] = max(dp[j],dp[j - k * v] + k * w);
		}
		
		for(int j = m;j >= s*v;j --)dp[j] = max(dp[j],dp[j - s * v]+ s* w);
	}
	cout << dp[m] << endl;
	return 0;
}

单调队列优化多重背包:

另见:算法学习记录:滑动窗口

优化效果(时间复杂度):

二维费用背包:

问题描述:

有一个体积为V的背包,商店有n种物品,每种物品有一个价值v、体积w、重量m,每种物品仅有1个,问能装下物品的最大价值。(需同时考虑体积和重量限制)

倒着更新,状态转移方程修改成二维。

状态转移方程:

 蓝桥OJ 3937:小蓝的神秘行囊
#include<bits/stdc++.h>
using namespace std;
using ll = long long ;
const int N = 105;
ll dp[N][N];

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

 分组背包:

有一个体积为V的背包,商店有n组物品,每组物品有若干个,价值v、体积w。每组物品至多选一个,问能够装下的最大价值。

设状态dp[i][j]表示到第i组,体积为j的最大价值这里不能忽略第一维,否则状态转移错误。

状态转移方程:

 蓝桥OJ 1178:小明的背包5
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1500;
ll dp[N][N];

int main()
{
	int n,V;cin >> n >> V;
	
	for(int i = 1;i <= n;i ++)
	{
		int s;cin >> s;
		for(int j = 0;j <= V;j ++)dp[i][j] = dp[i - 1][j];
		while(s --)
		{
			ll w,v;cin >> w >> v;
			for(int j = w;j <= V;j ++)dp[i][j] = max(dp[i][j],dp[i - 1][j - w] + v);
		}
	}
	
	
	cout << dp[n][V] << endl;
	
	return 0;
}

模型二:树型DP

了解过了树的基础后:算法学习记录:有关树的基础

 自上而下DP:

考虑树型DP问题一:

 如果暴力枚举:时间复杂度为O(n*2^n)不能解决问题

考虑贪心的办法:发现即使使用最大权值和作为判断依据,难以对树状结构进行正确计算。

因此采用动态规划算法:

考虑状态

  由题中限制一个子结点能不能选,受上一个结点的约束

所以需要开二维dp,用0或1表示被选或未被选的状态。

用f[i][0]表示当前结点不选所能得到的最大值

用f[i][1]表示当前结点选择后所能得的最大值。

考虑转移:

自上而下转移:在结点的所有儿子结点中取最大值,对0/1的情况分类讨论。

归纳出状态转移方程:

#include <bits/stdc++.h>
using namespace std;
#define maxn 110000
int n, val[maxn];
struct Edge
{
    int nex, to;
}edge[maxn << 1];
int head[maxn], cnt;
int f[maxn][2];
void add(int from, int to)
{
    edge[++cnt].nex = head[from];
    head[from] = cnt;
    edge[cnt].to = to;
    return ;
}
void dfs(int u, int fa)
{
    for (int i = head[u]; i; i = edge[i].nex)
    {
        int v = edge[i].to;
        if (v != fa)
            continue;
        dfs(v, u);
        f[u][0] += max(f[v][0], f[v][1]);
        f[u][1] += f[v][0];
    }
    return ;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; ++ i )
        scanf("%d", &val[i]), f[i][1] = val[i];
    for (int i = 1; i < n; ++ i )
    {
        int u, v;
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }
    dfs(1, 0);
    printf("%d\n", max(f[1][0], f[1][1]));
    return 0;
}

 考虑树型DP问题二:

 考虑状态

承接上一个模型,会想到用一个三维的dp,

仔细观察发现,可以将根据体积关系优化掉0/1这一维。

图解:

由体积的数量关系可以判断这个父结点一定被选上了,这样实现了一维的优化。  

考虑转移: 

在每一个结点下,把它的所有子节点当作许多物品。

用01背包的思路,将“贡献”合并。

归纳出状态转移方程: 

这是最核心的部分:

1.需要开一个新的一维数组来存储临时情况,因为更新时会用到原数据,不可以直接覆盖

2.外层循环会遍历到每个子节点,所以对于数组nf[v],最后复制回f[u][v]的数据是遍历过所有子结点的最终数据,是每个体积下的合法最优状况。

#include <bits/stdc++.h>
using namespace std;
#define maxn 110
int n, V;
int f[maxn][maxn];
int w[maxn], v[maxn];
vector<int> g[maxn];
struct Edge
{
	int nex, to;
}edge[maxn << 1];
int head[maxn], cnt;
void add(int from, int to)
{
	edge[++ cnt].nex = head[from];
	head[from] = cnt;
	edge[cnt].to = to;
	return ;
}

void dfs(int u, int fa)
{
	memset(f[u], -0x3f, sizeof f[u]);
	if (v[u] <= V)
		f[u][v[u]] = w[u];
	for (int i = head[u]; i; i = edge[i].nex)
	{
		int v = edge[i].to;
		if (v == fa)
			continue;
		dfs(v, u);
		vector<int> nf(f[u], f[u] + V + 1);
		for (int v1 = 0; v1 <= V; v1 ++)
		{
			for (int v2 = 0; v1 + v2 <= V; v2 ++ )
			{
				nf[v1 + v2] = max(nf[v1 + v2], f[u][v1] + f[v][v2]);
			}
		}
		for (int v = 0; v <= V; v ++ )
			f[u][v] = nf[v];
	}
	return ;
}
int main()
{
	scanf("%d%d", &n, &v);
	for (int i = 1; i < n; ++ i )
	{
		int u, v;
		add(u, v), add(v, u);
	}
	dfs(1, 0);
	int ans = 0;
	for (int i = 0; i <= V; ++ i )
		ans = max(ans, f[1][i]);
	cout << ans << endl;
	return 0;
}


 自下而上DP:

 每一次进行转移时,先遍历子节点,求出子节点的DP值之后,再向父节点转移。

最大独立集:

蓝桥OJ 1319:蓝桥舞会 

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
int n,a[N];
long long dp[N][2];
vector<int> e[N];
void dfs(int u)
{
    for (auto v:e[u])
    {
        dfs(v);
        dp[u][1]+=dp[v][0];
        dp[u][0]+=max(dp[v][0],dp[v][1]);
    }
    dp[u][1]+=a[u];
}
int main()
{
    cin>>n;
    set<int> st;
    for (int i=1;i<=n;i++) cin>>a[i],st.insert(i);
    for (int i=1,x,y;i<n;++i)
    {
        cin>>x>>y;
        e[y].push_back(x);
        st.erase(x);
    }
    int rt=*st.begin();
    dfs(rt);
    cout<<max(dp[rt][0],dp[rt][1]);
    return 0;
}
最小点覆盖:

选择若干个点,使得树上每一条边都被覆盖。

即每一条边都至少有一个端点被选择,求被选择的点权和的最值。

 

 

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
vector<int>e[N];
int val[N],dp[N][2];
int d[N];
int n, m, k;
void dfs(int u,int fa)
{
    for (auto v:e[u])
    {
        if (v==fa) continue;
        dfs(v,u);
        dp[u][0]+=dp[v][1];
        dp[u][1]+=min(dp[v][0],dp[v][1]);
    }
    dp[u][1]+=1;
}
int main()
{
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    cin>>n;
    for (int i=1,x,y;i<n;++i)
    {
        cin>>x>>y;
        e[x].push_back(y);
        e[y].push_back(x);
    }
    dfs(1,0);
    cout<<min(dp[1][0],dp[1][1]);
    return 0;
}
最小支配集:

选择若干个点,使得树上的每一个点都被支配,即每一点要么自身被选择要么相邻的结点被选择。

求选择的点的点权和最值。 

模型三:区间DP

区间DP是以区间为尺度的DP,一般有以下特点:

1.可以将一个大区间的问题拆成若干个子区间合并的问题

2.两个连续的子区间可以进行整合、合并成一个大区间。

 模型题:

#include <bits/stdc++.h>
using namespace std;
const int N=100005;
vector<int>e[N];
long long a[N],dp[N][3];
int d[N];
int n;
void dfs(int u)
{
    long long minn=1e18;
    for (auto v:e[u])
    {
        dfs(v);
        dp[u][0]+=min({dp[v][0],dp[v][1],dp[v][2]});
        dp[u][1]+=min(dp[v][0],dp[v][1]);
        minn=min(minn,dp[v][0]-min(dp[v][0],dp[v][1]));
        dp[u][2]+=dp[v][1];
    }
    dp[u][0]+=a[u];
    dp[u][1]+=minn;
}
int main()
{
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    cin>>n;
    for (int i=1;i<=n;++i)
    {
        int id,m;
        cin>>id;
        cin>>a[id];
        cin>>m;
        while (m--)
        {
            int x;
            cin>>x;
            e[id].push_back(x);
            d[x]++;
        }
    }
    int rt;
    for (int i=1;i<=n;++i)
        if (d[i]==0) rt=i;
    dfs(rt);
    cout<<min(dp[rt][0],dp[rt][1]);
    return 0;
}

模型四:状压DP

  状态压缩就是用某种方法来表示某种状态,通常是用一串01数字(二进制数)来表示各个状态。

这类题综合运用:位运算、前缀和、搜索等多种技巧,难度较高。

蓝桥OJ 186: 糖果
#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using Pair = std::pair<LL, int>;
#define inf 1'000'000'000'000'000'000ll

void solve(const int &Case) {
    int n, m, k;
    std::cin >> n >> m >> k;
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < k; j++) {
            int x;
            std::cin >> x;
            x--;
            a[i] |= 1 << x;
        }
    }
    std::vector<int> f(1 << m, n + 1);
    f[0] = 0;
    for (int i = 0; i < n; i++) {
        auto g = f;
        std::vector<int>(1 << m, n + 1).swap(f);
        for (int S = 0; S < 1 << m; S++) {
            f[S] = std::min(f[S], g[S]);
            f[S | a[i]] = std::min(f[S | a[i]], g[S] + 1);
        }
    }
    std::cout << (f[(1 << m) - 1] > n ? -1 : f[(1 << m) - 1]) << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}
蓝桥OJ 1261:小明的宠物袋
// 对于第 i 层第 j 个格子能不能放宠物,我们肯定需要知道第 i - 1 层第 j 个格子有没有放宠物,且第 i 层第 j - 1 个格子有没有放宠物
// 状压,状态 T 的第 j 位表示上一层的第 j 个格子有没有放宠物,如果有是 1,没有就是 0,此时 S 就是 m 位二进制数
// f[i][T] 表示处理了前 i 层,第 i 层状态为 T 的最大宠物数量
// 我们枚举了当前层状态为 T,上一层状态为 S,根据上述信息,我们有以下几个限制:
// 1. 如果 S[j] = 1, 此时 T[j] 必须为 0,从二进制角度来看,就是不能有同一位同时为 1,即 T and S = 0
// 2. 如果 T[j] = 1, 此时 T[j + 1] 必须为 0,这启发我们预处理出合法的状态 T,即暴力枚举所有 T, 判断是否满足条件
// 3. 如果 a[i][j] = 1, 此时 T[j] 必须为 0,从二进制角度来看,就是不能有同一位同时为 1, 即 T and a[i] = 0
// 重新捋一下三个限制:
// 1. T and S = 0
// 2. T[j] and T[j + 1] = 0
// 3. T and a[i] = 0

#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using Pair = std::pair<int, int>;
#define inf 1'000'000'000'

void solve(const int &Case) {
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        int S = 0;
        for (int j = 0; j < m; j++) {
            int x;
            std::cin >> x;
            S = S * 2 | x;
        }
        a[i] = S;
    }
    std::vector<int> ban(1 << m);
    for (int S = 0; S < 1 << m; S++) { // 预处理出所有满足 T[j] and T[j + 1] = 0 的 T
        // ban[S] = 1 表示不满足
        for (int i = 0; i < m - 1; i++) {
            if ((S >> i & 1) && (S >> (i + 1) & 1)) {
                ban[S] = 1;
                break;
            }
        }
    }
    std::vector<int> f(1 << m, -1);
    f[0] = 0;
    for (int i = 0; i < n; i++) {
        auto g = f;
        std::vector<int>(1 << m, -1).swap(f);
        for (int T = 0; T < 1 << m; T++) {
            if (T & a[i] || ban[T])continue; // 合法的 T 要求 T and a[i] = 0 而且 ban[T] = 0
            // S and T = 0
            // (1 << m) - 1 为 m 位二进制数,且每一位都是 1
            // T xor ((1 << m) - 1) 等价于 T 每一位都取反, 即 0 变 1, 1 变 0
            // 此时 S and T = 0 等价于 S 是 T xor ((1 << m) - 1) 的子集
            // 然后枚举子集
            // __builtin_popcount(x) 表示的是 x 二进制位中 1 的个数,记不住这个函数可以自己提前预处理
            int S = T ^ ((1 << m) - 1), v = __builtin_popcount(T);
            for (int nS = S; nS > 0; nS = (nS - 1) & S) { // 枚举 nS 是 S 的子集,且按照字典序降序,建议记住
                if (g[nS] == -1)continue;
                f[T] = std::max(f[T], g[nS] + v);
            }
            f[T] = std::max(f[T], g[0] + v); // 注意上面的代码中,nS 不能等于 0,所以这里再特殊处理一下
        } // 这一串代码时间复杂度是 O(3 ^ m)
        // 参考位运算那一章节
    }
    std::cout << *std::max_element(f.begin(), f.end()) << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}

高维前缀和:

 

#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using Pair = std::pair<int, int>;
#define inf 1'000'000'000'

void solve(const int &Case) {
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        int S = 0;
        for (int j = 0; j < m; j++) {
            int x;
            std::cin >> x;
            S = S * 2 | x;
        }
        a[i] = S;
    }
    std::vector<int> ban(1 << m);
    for (int S = 0; S < 1 << m; S++) {
        for (int i = 0; i < m - 1; i++) {
            if ((S >> i & 1) && (S >> (i + 1) & 1)) {
                ban[S] = 1;
                break;
            }
        }
    }
    std::vector<int> f(1 << m, -1);
    f[0] = 0;
    for (int i = 0; i < n; i++) {
        auto g = f;
        std::vector<int>(1 << m, -1).swap(f);
        for (int j = 0; j < m; j++) {
            for (int S = 0; S < 1 << m; S++) {
                if (S >> j & 1) {
                    g[S] = std::max(g[S], g[S ^ (1 << j)]);
                }
            }
        }
        for (int T = 0; T < 1 << m; T++) {
            if (T & a[i] || ban[T])continue;
            int S = T ^ ((1 << m) - 1), v = __builtin_popcount(T);
            f[T] = g[S] + v;
        }
    }
    std::cout << *std::max_element(f.begin(), f.end()) << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}

模型五:数位DP

这类题的一般形式:有一个很大的n,求1~n内有多少个数,满足某一个形式。 

蓝桥OJ 836:Windy数

两种方法:迭代法、dfs记忆化搜索

应用技巧:前缀和、枚举

 迭代法:

 设计状态:

  dp数组用于预处理,一维用于记录当前枚举到的长度,另一维用于记录目前添加的这个数

即,dp[i][j]表示长度为i,当前处理到的这一位数为j的所有windy数个数。

状态转移:
 dp[i][j] += dp[i-1][k]

问题转化:

根据前缀和的原理:

求区间[l,r]的windy数个数======求区间(0,r]的windy数个数、求区间(0,l-1]的windy数个数

总结:

  状态的转移较易,难在如何进行分类讨论,找出答案:

例:

现:通过预处理,dp数组已存下了所有情况下的windy数的个数。

考虑一个数:7430

情况一:最高位小于7的所有windy数

情况二:所有3位数的windy数(位数比边界数字的位数小的windy数)

情况三:紧贴边界的情况

对7430来说,从最高位开始考虑紧贴边界

74xx可行,此时7-4=3>=2,可以紧贴边界

73xx可行

72xx可行

71xx可行

70xx可行

再考虑74xx这种紧贴边界的情况:

743x不可行,由于4-3=1<2,不能再紧贴边界(使用break语句,跳出循环)

742x可行

741x可行

740x可行

以上可行的情况,只需给ans加上相应的dp[i][j],就可以不重不漏地找到答案。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int dp[20][20];
//dp[i][j]表示长度为i的数字,最高位是j的小明数的个数
int dig[20];//存储一个数字的每一位,如123 -> 1 2 3

ll solve(int x)
{
	memset(dig,0,sizeof(dig)); 
	int cnt=0;//cnt为x的长度 
	while(x)//拆分x,如123 -> 1 2 3 
	{
		dig[++cnt]=x%10;
		x=x/10;
	}
	
	ll ans=0;
	for(int i=1;i<cnt;i++)//对于所有长度小于cnt的数字,放心累加
	{
		for(int j=1;j<=9;j++)//枚举最高位所有可能的数字0~9
		{
			ans+=dp[i][j];    
		}    
	} 
	
	for(int i=1;i<dig[cnt];i++)//长度等于cnt,但是最高位比x的最高位小
	{
		ans+=dp[cnt][i];
	}
	
	for(int i=cnt-1;i>=1;i--)//枚举当前填到哪一位
	{
		for(int j=0;j<=dig[i]-1;j++)//枚举第一位填什么,从0到当前次高位减1
		{
			if(abs(j-dig[i+1])>=2)//次高位与最高位的差大于等于2
			{
				ans+=dp[i][j];
			}
		}
		if(abs(dig[i+1]-dig[i])<2)break;//不能填这个数字
	}
	return ans;
}

int main()
{
	int L,R;
	cin>>L>>R;
	memset(dp,0,sizeof(dp));
	for(int i=0;i<=9;i++)dp[1][i]=1;//初始化,长度为1,一定有一个
	
	for(int i=2;i<=10;i++)//枚举当前数字的位数 
	{
		for(int j=0;j<=9;j++)//当前数字的最高位 
		{
			for(int k=0;k<=9;k++)//枚举长度为i-1的数字(上一个状态)的最高位 
			{
				if(abs(j-k)>=2)//最高位差值的绝对值小于等于K,满足条件 
				{
					//可以从上一个状态转移而来 
					dp[i][j] += dp[i-1][k];//累加小明数的数量 
				}
			}
		}
	}
	
	ll ans=solve(R)-solve(L-1);
	cout<<ans<<endl;    
	
	return 0;
}

DFS记忆化搜索: 

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define maxn 100
ll a,b,len,dig[maxn];
ll f[20][20][2][2];

ll dfs(int len,int last,int flag,int zero){
	if(!len)return 1;
	if(f[len][last][flag][zero] != -1)return f[len][last][flag][zero];
	ll res = 0;
	int low,up;
	if(flag == 0)up = 9;
	else up = dig[len];
	if(zero == 1)low = 1;
	else low = 0;
	for(int i = low;i <= up;i ++) {
		if(abs(i - last) < 2)continue;
		res+=dfs(len - 1,i,flag&&(i == up),0);
	}
	f[len][last][flag][zero] = res;
	return res;
}


ll solve(ll x){
	if(x <= 9)return x;
	ll tmp = x;len = 0;
	while(tmp){
		dig[++len] = tmp % 10;
		tmp/=10;
	}
	memset(f,-1,sizeof(f));
	ll ans = 0;
	ans = dfs(len,11,1,1);
	for(int i = len - 1;i >= 1;i --) {
		ans += dfs(i,11,0,1);
	}
	return ans;
}
int main(){
	
	scanf("%lld%lld",&a,&b);
	printf("%lld\n",solve(b)-solve(a-1));
	
	return 0;
}
蓝桥OJ 2955:小熊的困惑
// 记录乘积肯定不行,乘积会有非常多种
// 接下来的除法 / 都是下整除运算
// 但注意到 n / 1, n / 2, n / 3, ..., n / n 只有 2sqrt(n) 种不同的数
// 证明如下:n / i, 如果此时 i > sqrt(n), n / i <= sqrt(n), 那么此时 n / i 只有 sqrt(n) 种
// 若此时 i <= sqrt(n), i 只有 sqrt(n) 种, 那么此时 n / i 只有 sqrt(n) 种
// 所以总共只有 2sqrt(n) 种
// i * j <= m <=> i <= m / j
// (m / i) / j = m / (i * j)


#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using unl = __int128;
using Pair = std::pair<int, int>;
#define inf 1'000'000'000'000'000ll'

void solve(const int &Case) {
    LL n;
    int m;
    std::cin >> n >> m;
    // 这里为了方便,我直接使用 map 来记录 dp 状态
    std::map<Pair, LL> M;
    // 实际上, 记录 n / i 有种很好的方法, 是记录所有小于等于 sqrt 的数字,然后用两个下标映射数组
    // 感兴趣可以学习 min25 筛,在 min25 筛中用到
    std::function<LL(int, int, int, int,
                     const std::vector<int> &)> dp = [&](int dep, int up, int zero, int div,
                                                         const auto &a) {
        if (dep == -1)return (LL) ((zero ^ 1) && div != 0);
        if (up == 0 && zero == 0 && M.count({dep, div}))return M[{dep, div}];
        int lim = a[dep];
        if (!up)lim = 9;
        LL ret = 0;
        if (div > m) { // 如果数位中出现 0
            for (int i = 0; i <= lim; i++) {
                ret += dp(dep - 1, up && i == lim, zero && i == 0, div, a);
            }
        } else {
            for (int i = 0; i <= lim; i++) {
                if (i == 0) { // 有 0 时则特殊赋值 div
                    if (zero == 1)ret += dp(dep - 1, up && i == lim, 1, div, a);
                    else ret += dp(dep - 1, up && i == lim, 0, m + 1, a);
                    continue;
                }
                ret += dp(dep - 1, up && i == lim, 0, div / i, a);
            }
        }
        if (up == 0 && zero == 0)M[{dep, div}] = ret;
        return ret;
    };
    auto get = [&](LL x) {
        std::vector<int> a;
        while (x > 0)a.push_back((int) (x % 10)), x /= 10;
        int n = (int) a.size();
        return dp(n - 1, 1, 1, m, a);
    };
    std::cout << get(n) << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}

模型六:期望DP

又名为概率DP,用于解决概率问题与期望问题。

求概率正推,求期望倒推

蓝桥OJ 4337:小蓝玩游戏

// f[i][j] 
// = (f[i][j] + 1) * i / n * j / m + (f[i + 1][j] + 1) * (n - i) / n * j / m 
//      + (f[i][j + 1] + 1) * i / n * (m - j) / m + (f[i + 1][j + 1] + 1) * (n - i) / n * (m - j) / m
// f[i][j] 
// = (f[i][j + 1] * i * (m - j) + f[i + 1][j] * j * (n - i) 
//      + f[i + 1][j + 1] * (n - i) * (m - j) + n * m) / (n * m - i * j)

#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using unl = __int128;
using Pair = std::pair<int, int>;
#define inf 1'000'000'000'000'000ll'

void solve(const int &Case) {
    int n, m;
    std::cin >> n >> m;
    std::vector<std::vector<double>> f(n + 1, std::vector<double>(m + 1));
    for (int i = n; i >= 0; i--) {
        for (int j = m; j >= 0; j--) {
            if (i == n && j == m)continue;
            if (j < m)f[i][j] += f[i][j + 1] * i * (m - j);
            if (i < n)f[i][j] += f[i + 1][j] * j * (n - i);
            if (i < n && j < m)f[i][j] += f[i + 1][j + 1] * (n - i) * (m - j);
            f[i][j] = (f[i][j] + n * m) / (n * m - i * j);
        }
    }
    std::cout << std::fixed << std::setprecision(4) << f[0][0] << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}
/*
#include <bits/stdc++.h>
using namespace std;
 
#define endl '\n'
#define LL long long
#define ph push_back
#define inf 0x3f3f3f3f
#define PLL pair<LL, LL>
#define INF 0x3f3f3f3f3f3f3f3f
 
const int N = 1e3 + 5;
 
int n, m;
double dp[N][N];
 
int main() {
    cin >> n >> m;
    
    dp[n][m] = 0;
    for (int i = n; i >= 0; i --)
        for (int j = m; j >= 0; j --) {
            if (i == n && j == m) continue;
            dp[i][j] = (i * (m - j) * dp[i][j + 1] + (n - i) * j * dp[i + 1][j] + (n - i) * (m - j) * dp[i + 1][j + 1] + n * m) / (n * m - i * j);
        }
    printf("%.4lf\n", dp[0][0]);
    return 0;
}
*/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值