在动态规划中最重要的就是要确定转移方程所确定的含义,当确定了转移方程的含义之后才能考虑这样的写法是否正确,并且如何初始化,考虑清楚对写出题目帮助非常大,并且很多时候dp数组的定义方式是有些套路的,所以多做题才能有感觉
1.从n个数字里面选择刚好k个凑出的数有多少种情况
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=110;
int a[maxn],b[maxn];
int dp[maxn][maxn][maxn];
void solve(){
int n,k;
cin>>n>>k;
int sum=0;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];
}
dp[0][0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=k;j++){
for(int k=0;k<=sum;k++){
dp[i][j][k]|=dp[i-1][j][k]; //从前面i个数里面刚好选择j个凑出k
if(k>=a[i])dp[i][j][k]|=dp[i-1][j-1][k-a[i]];
}
}
}
cout<<"sum "<<sum<<"\n";
for(int i=0;i<=sum;i++){
cout<<"i "<<i<<"val ";
cout<<dp[n][k][i]<<"\n";
}
}
signed main(){
int t=1;
while(t--){
solve();
}
}
2.从n个数里面选择0-k个凑出的数有多少种情况
第一种做法:重点在于 if(j)dp[i][j][k]|=dp[i][j-1][k] 代表前i个里面选择刚好j个等于k或等于前i个里面选择刚好j-1个等于k
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=110;
int a[maxn],b[maxn];
int dp[maxn][maxn][maxn];
void solve(){
int n,k;
cin>>n>>k;
int sum=0;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];
}
dp[0][0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=k;j++){
for(int k=0;k<=sum;k++){
if(j){
dp[i][j][k]|=dp[i][j-1][k];
}
dp[i][j][k]|=dp[i-1][j][k]; //从前面i个数里面刚好选择0-j个凑出k
if(k>=a[i])dp[i][j][k]|=dp[i-1][j-1][k-a[i]];
}
}
}
cout<<"sum "<<sum<<"\n";
for(int i=0;i<=sum;i++){
cout<<"i "<<i<<"val ";
cout<<dp[n][k][i]<<"\n";
}
}
signed main(){
int t=1;
while(t--){
solve();
}
}
第二种做法: for(int i=0;i<=n;i++){
dp[0][i][0]=1;
} 重点在于初始化的时候要注意从前面0个里面选择i个组成0的方案数有多少种,一开始我以为要把dp[i][j][k]里面的i和j都要变化其实不然,对于dp方程一般i都是由i-1转移过来的所以初始化的时候只要把i=0时候的各种情况初始化好即可i=1,2,3可以由上一个转移过来
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=110;
int a[maxn],b[maxn];
int dp[maxn][maxn][maxn];
void solve(){
int n,k;
cin>>n>>k;
int sum=0;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];
}
for(int i=0;i<=n;i++){
dp[0][i][0]=1;
}
for(int i=1;i<=n;i++){
for(int j=0;j<=k;j++){
for(int k=0;k<=sum;k++){
if(j){
dp[i][j][k]|=dp[i][j-1][k];
}
dp[i][j][k]|=dp[i-1][j][k]; //从前面i个数里面刚好选择0-j个凑出k
if(k>=a[i])dp[i][j][k]|=dp[i-1][j-1][k-a[i]];
}
}
}
cout<<"sum "<<sum<<"\n";
for(int i=0;i<=sum;i++){
cout<<"i "<<i<<"val ";
cout<<dp[n][k][i]<<"\n";
}
}
signed main(){
int t=1;
while(t--){
solve();
}
}
3.从n个数里面任意选择选出k的方案数量
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=110;
int a[maxn],b[maxn];
int dp[maxn][maxn];
void solve(){
int n,k;
cin>>n>>k;
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<=k;j++){
dp[i][j]=dp[i-1][j]+dp[i-1][j-a[i]];
}
}
cout<<dp[n][k]<<"\n";
}
signed main(){
int t=1;
while(t--){
solve();
}
}
4.给定一个简单无向图,给你起点s和终点t,点可以重复经过,要你求出从s-t经过点x偶数次,路径长度为k的方案个数
这题看上去感觉像是树形dp但是你写了就会发现树形dp非常不可写(可能是我太菜了),所以有一种做法是dp,假设dp[i][j]为经过路径长度为i目前在j的方案个数,则dp[0][s]=1 dp[i][j]=dp[i-1][w] w是与j直接相连的点
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e3+5;
const int mod=998244353;
int dp[maxn][maxn][2];
vector<int>vec[maxn];
signed main(){
int n,m,k,s,t,x;
cin>>n>>m>>k>>s>>t>>x;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
vec[u].push_back(v);
vec[v].push_back(u);
}
dp[0][s][0]=1;
for(int i=1;i<=k;i++){
for(int j=1;j<=n;j++){
for(auto u:vec[j]){
for(auto it:{0,1}){
if(j==x){
dp[i][j][it]=(dp[i][j][it]+dp[i-1][u][1-it])%mod;
}
else{
dp[i][j][it]=(dp[i][j][it]+dp[i-1][u][it])%mod;
}
}
}
}
}
cout<<dp[k][t][0]<<"\n";
}
5.最长上升子序列
记录dp[i]为上升长度为i的子序列,对应的最小的结尾元素 则可以有nlogn写法
注意这里有一个扩展的问题如luogu这一道,问你需要几套导弹设备就相当于问你最长上升子序列长度是多少?
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9+7;
const int maxn=2e5+5;
int a[maxn];
int dp[maxn];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int len=1;
memset(dp,inf,sizeof(dp));
dp[1]=1;
for(int i=2;i<=n;i++){
if(a[i]>dp[len]){
dp[++len]=dp[i];
}
else{
int pos=lower_bound(dp+1,dp+1+len,a[i])-dp;
dp[pos]=a[i];
}
}
cout<<len+1<<"\n";
}
6.最长公共子串
dp[i][j]代表a数组的前面i位和b数组的前面j位的最长公共子序列长度
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
int a[maxn],b[maxn];
int dp[maxn][maxn]; //代表a数组的前i位和b数组的前j位的最长公共字串长度
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(a[i]==b[j]){
dp[i][j]=dp[i-1][j-1]+1;
}
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
cout<<dp[n][n]<<"\n";
}
7.从n个数(有正有负)里面选取任意w个数凑出目标k的方案有多少种
对于有负数的情况我们需要进行位移一个中间值zhong,然后第二个循环里面不能写0到k+zhong,因为这里的k+zhong不一定都是由i-1里面小于等于k+zhong而转移过来的,也有可能a[i]是负数,i-1的时候更大而转移过来
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e5+5;
int a[105];
int dp[105][maxn];
int main(){ //求凑出need的方案数,并且有可能有负数
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int zhong=2e5;
int need;
cin>>need;
dp[0][zhong]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=4e5;j++){
dp[i][j]+=dp[i-1][j];
if(j-a[i]>=0&&j-a[i]<=4e5){
dp[i][j]+=dp[i-1][j-a[i]];
}
}
}
cout<<dp[n][need+zhong];
}
8.多重背包二进制优化
多重背包问题通常可转化成01背包问题求解。但若将每种物品的数量拆分成多个1的话,时间复杂度会很高,从而导致TLE。所以,需要利用二进制优化思想。即:
一个正整数n,可以被分解成1,2,4,…,2^(k-1),n-2^k+1的形式。其中,k是满足n-2^k+1>0的最大整数。
例如,假设给定价值为2,数量为10的物品,依据二进制优化思想可将10分解为1+2+4+3,则原来价值为2,数量为10的物品可等效转化为价值分别为1*2,2*2,4*2,3*2,即价值分别为2,4,8,6,数量均为1的物品。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int v[maxn],w[maxn],s[maxn];
int dp[maxn];
void solve(){
int n,vv;
cin>>n>>vv;
int cnt=0;
int wi,vi,si;
for(int i=1;i<=n;i++){
cin>>wi>>vi>>si;
int k=1;
while(k<=si){ //例如s[i]=10 则被分为了1 2 4 3
cnt++;
w[cnt]=wi*k;
v[cnt]=vi*k;
si-=k;
k*=2;
}
if(si>0){
cnt++;
w[cnt]=wi*si;
v[cnt]=vi*si;
}
}
n=cnt;
for(int i=1;i<=n;i++){
for(int j=vv;j>=1;j--){
if(j>=w[i]){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else{
dp[j]=dp[j];
}
}
}
cout<<dp[vv];
}
int main(){
int t;
t=1;
while(t--){
solve();
}
}
9.分组背包
有n组物品,每组物品你至多可以选择其中1个,你现在有vv的容量要求最后的价值最高
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e2+5;
int v[maxn][maxn],w[maxn][maxn],s[maxn];
int dp[maxn][maxn];
void solve(){
int n,vv;
cin>>n>>vv;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=1;j<=s[i];j++){
cin>>w[i][j]>>v[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=vv;j++){
dp[i][j]=dp[i-1][j];
for(int k=1;k<=s[i];k++){
if(j>=w[i][k]){
dp[i][j]=max(dp[i][j],dp[i-1][j-w[i][k]]+v[i][k]);
}
}
}
}
cout<<dp[n][vv];
}
int main(){
int t;
t=1;
while(t--){
solve();
}
}
10.通过删除添加替换把字符串A变成B需要的最小操作数
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
char a[maxn],b[maxn];
int dp[maxn][maxn];
const int inf=1e9+7;
int main(){
int n,m;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cin>>m;
for(int i=1;i<=m;i++){
cin>>b[i];
}
memset(dp,inf,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=max(n,m);i++){
dp[i][0]=i;
dp[0][i]=i;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i]==b[j]){
dp[i][j]=min(min(dp[i][j],min(dp[i-1][j-1],dp[i][j-1]+1)),dp[i-1][j]+1);
}
else{
dp[i][j]=min(min(dp[i][j],min(dp[i-1][j-1]+1,dp[i][j-1]+1)),dp[i-1][j]+1);
}
}
}
//cout<<dp[0][25]<<"\n";
cout<<dp[n][m];
}
11.给定一个a数组,里面的元素都为正数,每次可以选择一段不包含0的区间减一,求把区间里面所有元素减到0的最少次数
int count(int a[],int n){
dp[1]=a[1];
for(int i=2; i<=n; i++)
if(a[i] <= a[i-1]) {
dp[i]=dp[i-1];
} else {
dp[i]=dp[i-1]+(a[i]-a[i-1]);
}
return dp[n];
}
12.单调队列优化多重背包
对于一个体积为v的物体来说,要求它的贡献则可以枚举余数0到v-1,然后再枚举这个物体的个数,那么对于现有k空间的背包来说,它的由该物体更新而来的最大值就等于max(dp[k-v],dp[k-2v],dp[k-3v]...),那么可以用单调队列维护余数为i的最大值,则单调队列优化多重背包时间复杂度就等于O(n*m) 如果用二进制优化的话,假设平均的物品体积为v,则二进制优化多重背包时间复杂度为O(n*m*log(v))
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e4+5;
int n,m;
int dp[maxn],g[maxn],q[maxn];
signed main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
memcpy(g,dp,sizeof(dp));
for(int j=0;j<v;j++){
int head=0,tail=-1;
for(int k=j;k<=m;k+=v){
if(head<=tail&&k-s*v>q[head]){
head++;
}
if(head<=tail){
dp[k]=max(dp[k],g[q[head]]+(k-q[head])/v*w);
}
while(head<=tail&&g[q[tail]]-(q[tail]-j)/v*w<=g[k]-(k-j)/v*w){
tail--;
}
q[++tail]=k;
}
}
}
cout<<dp[m]<<"\n";
}
13.背包问题求最优方案数
#include<bits/stdc++.h>
#define int long long
const int maxn=1005;
const int mod=1e9+7;
const int inf=1e9+7;
using namespace std;
int dp[maxn][maxn];
int num[maxn][maxn];
int cost[maxn],val[maxn];
signed main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>cost[i]>>val[i];
}
for(int i=0;i<=m;i++){
num[0][i]=1;
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
dp[i][j]=dp[i-1][j];
num[i][j]=num[i-1][j];
if(j>=cost[i]){
if(dp[i][j]<dp[i-1][j-cost[i]]+val[i]){
dp[i][j]=dp[i-1][j-cost[i]]+val[i];
num[i][j]=num[i-1][j-cost[i]];
num[i][j]%=mod;
}
else if(dp[i][j]==dp[i-1][j-cost[i]]+val[i]){
num[i][j]+=num[i-1][j-cost[i]];
num[i][j]%=mod;
}
}
}
}
cout<<num[n][m]<<"\n";
}
14.1-2022中选择10个互不相同的数,要求加起来等于2022求有多少种选择
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2222;
int dp[11][maxn];
signed main(){
dp[0][0]=1;
for(int i=1;i<=2022;i++){
for(int j=10;j>=1;j--){
for(int k=1;k<=2022;k++){
if(k>=i){
dp[j][k]+=dp[j-1][k-i];
}
}
}
}
cout<<dp[10][2022]<<"\n";
}
15.给定n,k,t,问:一个长度为n的数组,每个数为0到k-1,有多少数组满足以下条件:
该数组里面的连续子数组之和%k为0的子数组数量为t
思路:
首先考虑给定一个数组,怎么求这个数组有多少连续子数组满足和%k为0的数量?
可以前缀和%k处理该数组,例如 给定数组 [1 ,1 ,3] ,k为3,则处理后的数组为[1 , 2 ,2]那么可以发现前缀和数组里面相同的数之间之和%k是等于0的,所以假设这个前缀和数组里面各个数出现的次数为cnt0,cnt1,cnt2,...,那么贡献即为, (这里需要特殊给cnt0,因为什么都不选的时候前缀和为0也出现了一次)并且因为这个数组里面的每个数都属于0到k-1,所以可以任意形成任意的前缀和数组,并且一个前缀和数组对应唯一的原序列,那么我们就成功的把原问题转化为了有多少前缀和数组他们的贡献为t,定义dp数组为dp[i][j][k],代表考虑0到i-1这些数在前缀和数组里面加起来出现了j次,当前的贡献为k的情况有多少种,那么假设当前数出现了num次,那么当前数的贡献now即为C(num+(i==0?0:1),2),并且剩下的位置有n-len个,所以当前数的放置情况有C(n-len,num),所以综上转移方程即为:dp[i][j+now][len+num]+=dp[i][j][len]*C(n-len,num)
ps:最重要的就是这个前缀和数组的处理,很巧妙,可能也是一种套路吧,连续子数组%k为0即考虑前缀和处理
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=70;
const int mod=998244353;
int dp[maxn][maxn][maxn*maxn];
int c[maxn][maxn];
void solve(){
int n,k,t;
cin>>n>>k>>t;
c[0][0]=1;
for(int i=1;i<=68;i++){
c[i][0]=1;
for(int j=1;j<=i;j++){
c[i][j]=c[i-1][j-1]+c[i-1][j];
c[i][j]%=mod;
}
}
dp[0][0][0]=1;
for(int i=1;i<=k;i++){ //考虑前i个数
for(int len=0;len<=n;len++){ //已经占据了len个位置
for(int s=0;s<=t;s++){ //已经贡献了s个
for(int num=0;num<=n-len;num++){ //当前这个i出现多少次?
int now;
if(dp[i-1][len][s]==0){
continue;
}
if(i==k){
now=c[num+1][2];
}
else{
now=c[num][2];
}
if(now+s>t){
continue;
}
else{
//剩下的位置为n-len个,要放num个
int tmp=dp[i-1][len][s]*c[n-len][num];
tmp%=mod;
dp[i][len+num][s+now]+=tmp;
dp[i][len+num][s+now]%=mod;
}
}
}
}
}
cout<<dp[k][n][t]<<"\n";
}
signed main(){
int t=1;
//cin>>t;
while(t--){
solve();
}
}
题意:
给定一个长度为n的数组和q次操作,每次操作给定下标xy,可以选择交换a[x],a[y]或者什么都不做,所以总共有2的q次种最终的可能序列,问这些序列的总的逆序对的数量是多少?
思路:
定义dp[i][j][k]代表经过k次操作后当前a[i]<a[j]的情况有多少种,那么对于当前这次操作不难发现以下条件成立
但是这样转移的话,下面三条都可以O(n)转移,但是对于最上面一条我们枚举ij是O(n^2)的,这里有一个套路就是每次操作都把把这四条式子都除以2,那么最上面那条相当于没有变化,就不用动,下面三条可以O(n)得到,到最后算答案的时候再乘以2^m次即可
#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=3e3+5;
const int inf=1e9+7;
const int mod=1e9+7;
int a[maxn];
int dp[maxn][maxn];
int ksm(int x,int n){
int ans=1;
while(n){
if(n&1)ans=ans*x%mod;
x=x*x%mod;
n>>=1;
}
return ans;
}
void solve(){
int inv2=ksm(2,mod-2);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=(a[i]<a[j]);
}
}
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
dp[x][y]=dp[y][x]=(dp[x][y]+dp[y][x])%mod*inv2%mod;
for(int j=1;j<=n;j++){
if(j!=x&&j!=y){
dp[x][j]=dp[y][j]=(dp[x][j]+dp[y][j])%mod*inv2%mod;
}
}
for(int j=1;j<=n;j++){
if(j!=x&&j!=y){
dp[j][x]=dp[j][y]=(dp[j][x]+dp[j][y])%mod*inv2%mod;
}
}
}
int ans=0;
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
ans=(ans+dp[i][j])%mod;
}
}
ans=ans*ksm(2,m)%mod;
cout<<ans<<"\n";
}
signed main(){
int t=1;
//cin>>t;
while(t--){
solve();
}
}