动态规划刷题笔记

  • 洛谷P4933大师(线性DP)
  • 洛谷P5858Golden Sword(线性DP)
  • 洛谷P1280尼克的任务(线性DP)
  • USACO16OPEN 2048(区间DP)
  • 洛谷P2585三色二叉树(树形DP)
  • 洛谷P1441砝码称重(枚举,背包)
  • 洛谷P1896互不侵犯(状压DP)
  • codeforces1488E Palindromic Doubles(LIS+思维)
  • codeforces1486D Max Median(二分+思维)
  • codeforces1486E Paired Payment(多维最短路)
  • codeforces1426F Number of Subsequences(计数DP)

洛谷P4933大师

题意:给一个n长度序列,第i个数为h[i],求等差数列个数。(n<1e3,h[i]<2e4)
题解:由n和h[i]的范围可以知道,状态为dp[i][j],复杂度为n*max(h[j]),dp[i][j]代表以第i个数结尾j为公差的等差数列个数
枚举i,再枚举i的前一个,更新答案。

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
#define N 1005
#define Mod 998244353
#define maxh 20005
inline int read(){int x;scanf("%d",&x);return x;}
int h[N],dp[N][3*maxh];
void solve()
{
	int n=read(),ans=0;
	f(i,1,n) h[i]=read();
	f(i,1,n) f(j,1,i-1)
	{
		dp[i][h[i]-h[j]+maxh]=(dp[i][h[i]-h[j]+maxh]+dp[j][h[i]-h[j]+maxh]+1)%Mod;
		ans=(ans+dp[j][h[i]-h[j]+maxh]+1)%Mod;
	}
	ans=(ans+n)%Mod;
	cout<<ans;
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;
} 

洛谷P5858Golden Sword

题意:给n个原料,每个都有一个权值a[i](可能为负),依次放进锅里,锅的容量为w,在放进一个原料前,最多可以从锅里拿出s个,当一个原料放进锅里的时候,你会获得的 分数 =(放进当前原料锅中原料个数)*(当前原料的权值),求放n个原料后可以得到的最大权值,(n,w,s<5e5,a[i]<1e9)
题解:dp[i][j]代表放完i个原料且锅中现在有j个原料的最大分数,可以发现,dp[i][j]可以由dp[i-1][j-1],dp[i-1][j],dp[i-1][j+1],...,dp[i-1][j+s-1]转移而来,因此维护这一段的最大值即可,单调队列维护即可。

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register ll i=a;i<=b;++i)
#define ll long long
#define N 5005
#define Mod 998244353
#define INF 999999999
#define maxh 20005
inline ll read(){ll x;scanf("%lld",&x);return x;}
ll a[N],dp[N][N];
void solve()
{
	ll n=read(),w=read(),s=read();
	f(i,1,n) a[i]=read();
	dp[1][1]=a[1];
	f(i,2,n)
	{
		deque<ll>dq;
		if(i>w) dq.push_front(w);
		for(int j=min(w,i);j>=1;--j)
		{
			if(j!=1)
			{
				while(dq.size()&&dp[i-1][dq.front()]<=dp[i-1][j-1]) dq.pop_front();
				dq.push_front(j-1);
			}
			if(dq.back()>j-1+s) dq.pop_back();
			dp[i][j]=dp[i-1][dq.back()]+a[i]*j;
		}
	} 
	ll ans=-INF;
	f(i,1,min(w,i)) ans=max(ans,dp[n][i]);
	cout<<ans<<endl; 
}
int main()
{
	ll T=1;
	while(T--) solve();
	return 0;
} 

洛谷P1280尼克的任务

题意:有n单位的时间和m个任务,从第一个时间点开始,如果某个时间段没有执行任务,称为空暇时间,每个任务有个开始时间和结束时间,如果当前有x个任务开始前你没有执行任务,则必须选择一个,求怎么选择空暇时间最长。
题解:显然是线性DP,但是如果正向递推就会发现,特别的麻烦,于是采取倒推,设dp[i]代表从i开始到n的最长空暇时间,则从n到1递推,递推式为:

if(这个时间点没有任务开始) dp[i]=dp[i+1]+1;//
else 
 {
      for(所有的任务) dp[i]=max(dp[i],dp[i+T]) //T为当前任务结束时间
 }

AC代码

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define pb push_back
#define N 10005
inline int read(){int x;scanf("%d",&x);return x;}
int dp[N];
vector<int>pii[N];
int main()
{
	int n=read(),m=read();
	f(i,1,m)
	{
		int s=read(),t=read();
		pii[s].pb(t);
	}
	for(int i=n;i>=1;--i)
	{
		if(pii[i].size()==0) dp[i]=dp[i+1]+1;
		else
		{
			for(auto T:pii[i])
			{
				dp[i]=max(dp[i],dp[T+i]);
			}
		}
	}
	cout<<dp[1];
	return 0;
}

另一种方法:DAG求最长路(略)

USACO16OPEN 2048

题意:n个数的序列,用来玩2048,相邻两个相等的数可合并成这个数+1,求最大能合并的数
题解:如果一段区间可以合并成一个数,那这个数显然是确定的,dp[i][j]代表[i,j]区间合并成的数,如果不能合并,dp[i][j]=0;
转移方程:

for(int k=L;k<R;++k) 
      if(dp[L][k]&&dp[k+1][R]&&dp[L][k]==dp[k+1][R]) dp[L][R]=dp[L][k]+1;

AC代码

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define pb push_back
#define N 256
inline int read(){int x;scanf("%d",&x);return x;}
int dp[N][N],num[N];
int main()
{
	int n=read(),Max=0;
	f(i,1,n) num[i]=read();
	f(i,1,n) dp[i][i]=num[i],Max=max(Max,num[i]);
	f(len,2,n) f(L,1,n)
	{
		int R=L+len-1;
		if(R>n) break;
		f(k,L,R-1) if(dp[L][k]&&dp[k+1][R]&&dp[L][k]==dp[k+1][R]) dp[L][R]=dp[L][k]+1;
		Max=max(Max,dp[L][R]);
	}
	cout<<Max;
	return 0;
}

洛谷P2585三色二叉树

题意:给一颗二叉树,涂上红绿蓝三种颜色,要求父子和兄弟不能同色,求涂完色,绿色最多和最少有几个。((n<5*10^5)
题解:先考虑最多的情况,定义dp[i][0/1]代表以i为根的子树最多涂几个绿色,则得到以下转移式:

(dp1_{s,0}=max(dp1_{ls,1}+dp1_{rs,0},dp1_{ls,0}+dp1_{rs,1}))
(dp1_{s,1}=dp1_{ls,0}+dp1_{rs,0}+1)

考虑最少,得到以下转移式

(dp2_{s,0}=min(dp2_{ls,1}+dp2_{rs,0},dp2_{ls,0}+dp2_{rs,1}))
(dp2_{s,1}=dp2_{ls,0}+dp2_{rs,0})

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 550010;
char t[N];
int f[N][2], g[N][2], size[N];
inline void dfs(int x) 
{
	f[x][0] = 0; f[x][1] = 1;
	g[x][0] = 0; g[x][1] = 1;
	size[x] = 0;
	int to[3] = {0};
	for (int i = 1; i <= t[x] - '0'; i++) {
		int y = x + size[x] + 1;
		to[i] = y;
		dfs(y);
		size[x] += size[y];
	}
	size[x] += 1;
	if (t[x] == '0') return;
	if (t[x] == '1') {
		f[x][0] += max(f[to[1]][0], f[to[1]][1]);
		f[x][1] += f[to[1]][0];
		g[x][0] += min(g[to[1]][0], g[to[1]][1]);
		g[x][1] += g[to[1]][0];
	}
	if (t[x] == '2') {
		f[x][0] += max(f[to[1]][0] + f[to[2]][1], f[to[1]][1] + f[to[2]][0]);
		f[x][1] += f[to[1]][0] + f[to[2]][0];
		g[x][0] += min(g[to[1]][0] + g[to[2]][1], g[to[1]][1] + g[to[2]][0]);
		g[x][1] += g[to[1]][0] + g[to[2]][0];
	}
}
int main() {
	cin >> t;
	dfs(0);
	cout << max(f[0][0], f[0][1]) << " " << min(g[0][0], g[0][1]) << endl;
	return 0;
}

洛谷P1441砝码称重

题意:给(n)个砝码,选择(n-m)个,使得这(n-m)个砝码能组成的重量最多,输出最多组成几个重量((n<=20,m<=4,a_i<=100)).
题解:看数据范围像状压DP,实际上duck不必,枚举m个舍弃的砝码,每个枚举结果使用一次01背包记录,获得最大次数
枚举的复杂度:(C^{m}_{n})最多只有5000,每个结果dp,复杂度为(nsum a_i),最多为40000,总复杂度勉强能过。

#include <bits/stdc++.h>
using namespace std;
const int N = 550010;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
inline int read(){int x;scanf("%d",&x);return x;}
#define gc getchar()
#define pb push_back
#define Mod 80112002
int a[23],use[23],n,m,dp[2030],Ans;
void DP()
{
	memset(dp,0,sizeof dp);
	dp[0]=true;
	int sum=0,res=0;
	f(i,1,n)
	{
		if(use[i]) continue;
		for(int j=sum;j>=0;--j) if(dp[j]&&!dp[j+a[i]]) res++,dp[j+a[i]]=true;
		sum+=a[i];
	}
	if(res>Ans) Ans=res;
}
void dfs(int now,int pre)
{
	if(now==m)
	{
		DP(); 
		return ;
	}
	for(int i=pre+1;i<=n-m+now+1;++i)
	{
		use[i]=1;
		dfs(now+1,i);
		use[i]=0;
	}
}
int main() 
{
	n=read(),m=read();
	f(i,1,n) a[i]=read();
	dfs(0,0);
	cout<<Ans<<endl;
	return 0;
}

洛谷P1896互不侵犯

题意:每一个n*n地图,要在上面摆放m个国王,要求每个国王所在的九宫格内(国王可以放在边界)不能有其他国王,求摆放方案数。((n<=9,m<=n*n))
题解:根据数据范围可以发现是状压DP,定义:dp[i][j][state]代表到第i行摆放j个国王最后一行状态为state的方案数,对于可行的状态state,再枚举上一行的state,如果上一行和这一行状态不冲突,就把这个状态累加到当前状态。
要素1:对于一行状态是否可行,只要判断有没有相邻的1,巧妙利用位运算

bool check1(ll x)//判断一个状态是否合法 
{
	if(x&(x>>1)) return false;
	return true; 
}

要素2:先把一行所有的可行状态存进vector数组,枚举时会省下很多时间

f(i,0,tot)
	if(check1(i)) ok.pb(mp(i,bitcount(i)));//先把合法的状态保存起来,可以省下很多时间 

要素3:判断两个状态是否可以相邻出现,巧妙利用位运算

bool check2(ll x,ll y)//判断两个状态能否相邻 
{
	if(x&y) return false;
	if(x&(y<<1)) return false;
	if(x&(y>>1)) return false;
	return true;
}

AC代码:

#include <bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
inline ll read(){ll x;scanf("%lld",&x);return x;}
#define pb push_back
ll dp[10][100][550];
vector<pair<ll,ll> >ok;
#define mp make_pair
#define fi first
#define se second
ll bitcount(ll n)//返回一个状态1的个数 
{
    ll c =0 ;
    for(c=0;n;++c) n&=(n-1);
    return c ;
}
bool check1(ll x)//判断一个状态是否合法 
{
	if(x&(x>>1)) return false;
	return true; 
}
bool check2(ll x,ll y)//判断两个状态能否相邻 
{
	if(x&y) return false;
	if(x&(y<<1)) return false;
	if(x&(y>>1)) return false;
	return true;
}
int main() 
{
	ll n=read(),m=read(),tot=(1<<n)-1,ans=0; 
	f(i,0,tot)
		if(check1(i)) ok.pb(mp(i,bitcount(i)));//先把合法的状态保存起来,可以省下很多时间 
	dp[0][0][0]=1;//初始化很重要 
	f(i,1,n) 
	{
		ll Max=min(((n+1)/2)*((i+1)/2),m);
		f(j,0,Max)
		{
			if(!j){dp[i][0][0]=1;continue;}//特判j==0的情况 
			for(auto kp:ok)
			{
				if(kp.se>j) continue;
				for(auto qp:ok)
				{
					if(!check2(kp.fi,qp.fi)) continue;
					dp[i][j][kp.fi]+=dp[i-1][j-kp.se][qp.fi];
				}
			}
		}
		if(i==n) //到达最后一列,把答案加起来 
		{
			for(auto kp:ok)	ans+=dp[n][m][kp.fi];
		}	
	}
	cout<<ans<<endl;
	return 0;
}

codeforces1488E Palindromic Doubles

题意:给一个n长度序列a,(1<=a_i<=n),且每个数字最多出现两次,求最长回文子序列。
题解:先不考虑奇数长度的回文序列,偶数长度的回文序列由若干对相同的数字组成,我们拿出所有的存在两次的数字对,按照第一次出现的下标从小到大排序,再对第二次出现的下标求一次LIS,则偶数长度的最长回文子序列就是2*LIS。
现在考虑奇数长度,如果存在奇数长度的最长回文序列,则一定是最长的偶长回文序列长度+1,只要找出所有的最长偶长回文序列(也就是所有的LIS),再找出满足条件的一条,满足LIS的起点和与之对应的第一次出现的下标不相邻,就是可以构成奇数长度最长回文序列的LIS。

怎么求出所有的LIS?

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
const int N = 255000;
inline int read(){int x;scanf("%d",&x);return x;}
struct NUM{
	int x,y,id;
	NUM()
	{
		x=y=0;
		id=0;
	}
	void init()
	{
		x=y=0;
		id=0;
	}
	void add(int a)
	{
		if(!x) 
		{
			x=a;
			return ;
		}
		if(!y)
		{
			y=a;
			id=1;
			return ;
		}
	}
}num[N];
int cmp(NUM a,NUM b)
{
	return a.x>b.x;
}
int b[N],c[N],a[N],dp[N],cnt,ans,LIS[N],Max[N],vis[N];
void init(int n)
{
	ans=cnt=0;
	f(i,1,n)
	{
		vis[i]=0;
		Max[i]=0;
		LIS[i]=0;
		dp[i]=0;
		num[i].init();
	}
}
void solve()
{
	int n=read();
	init(n);
	f(i,1,n) num[read()].add(i);
	sort(num+1,num+1+n,cmp);
	f(i,1,n)
	{
		if(!num[i].id) continue;
		b[++cnt]=num[i].y;
		c[cnt]=num[i].x;
	}
	if(!cnt)
	{
		cout<<1<<endl;
		return ;
	}
	for(int i=1;i<=cnt;++i)
	{
		if(b[i]>dp[ans]) dp[++ans]=b[i],LIS[i]=ans;
		else
		{
			int x=upper_bound(dp+1,dp+1+ans,b[i])-dp;
			dp[x]=b[i];
			LIS[i]=x;
		}
	}
	for(int i=cnt;i>=1;--i)
	{
		if(LIS[i]==ans)
		{
			Max[LIS[i]]=max(Max[LIS[i]],b[i]);
			vis[i]=1;
			continue;
		}
		if(b[i]<Max[LIS[i]+1]) 
		{
			Max[LIS[i]]=max(Max[LIS[i]],b[i]);
			vis[i]=1;
		}
	}
	for(int i=1;i<=cnt;++i)
	{
		if(LIS[i]==1&&vis[i]==1)
		{
			if(b[i]-c[i]>1) 
			{
				cout<<2*ans+1<<endl;
				return ;
			}
		} 
	}
	cout<<2*ans<<endl;
}
int main()
{
	int T=read();
	while(T--) solve();
	return 0;
} 

codeforces1486D Max Median

题意:给一个n长度数组和一个k,求所有长度大于k的连续子序列中,最大的中位数是多少?
题解:首先二分答案,对每一个mid值判断是否可行,在一个大佬的博客里学到的技巧,对于每次二分后,将数组中大于等于mid的值变成1,其他的变成-1,这样当区间[L,R]的区间和大于等于1的时候,它的中位数就大于等于mid,这样就相当好做了。

#include<bits/stdc++.h>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
const int N = 255000;
int n,k,a[N],b[N],Min[N];
inline int read(){int x;scanf("%d",&x);return x;}
bool check(int mid)
{
	int sum=0,pre=0,mp=0;
	f(i,1,n)
		if(mid<=a[i]) b[i]=1;
	else b[i]=-1;
 	f(i,1,k) sum+=b[i];
 	if(sum>=1) return true;
 	f(i,k+1,n)
 	{
 		sum+=b[i];
 		pre=pre+b[i-k];
 		mp=min(mp,pre);
 		if(sum-mp>=1) return true;
	}
	return false;
}
void solve()
{
	n=read(),k=read();
	f(i,1,n) a[i]=read();
	int L=1,R=n;
	while(L<R)
	{
		int mid=(L+R+1)>>1;
		if(check(mid)) L=mid;
		else R=mid-1;
	}
	printf("%d
",L);
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;
} 

codeforces1486E Paired Payment

题意:给一张n点m边无向图,每条边有权值w,移动时,一次只能连续走两条边,代价为((w1+w2)^2),求从1到各点的最短路,若走不到输出-1。
题解:显然普通的最短路算法无法解决这个问题,因为从一个点到另一个点的转移不仅要看上一条边的权值。还要看当前走的边是奇边还是偶边。
【多维最短路】:dis[i][j][0/1]代表当前到达i点,上一条边权值为j,且这条边是奇/偶边的最短路,这样这题就转化成以dis[i][j][k]为节点相互转移的最短路问题,用一次Dijkstra就可以了。

#include<vector>
#include<queue>
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
inline ll read(){ll x;scanf("%lld",&x);return x;}
const int N = 100005;
vector<pair<ll,ll> >eg[N];
ll dis[N][55][2],n,m;
#define mp make_pair
#define pb push_back
const ll INF = 1e18;
ll ans[N];
struct Node{
	ll len,last,odd,id;
	Node(){}
	Node(ll l1,ll l2,ll o,ll i)
	{
		len=l1;
		last=l2;
		odd=o;
		id=i;
	}
	friend bool operator<(Node a,Node b)
	{
		return a.len>b.len;
	}
};
void dij()
{
	priority_queue<Node > qn;
	f(i,1,n) f(j,1,50) f(k,0,1) dis[i][j][k]=INF;
	dis[1][0][0]=0;
	qn.push(Node(0,0,0,1));
	while(qn.size())
	{
		ll len=qn.top().len,last=qn.top().last,odd=qn.top().odd,id=qn.top().id;
		qn.pop();
		if(len!=dis[id][last][odd]) continue;
		for(auto pv:eg[id])
		{
			ll v=pv.first,w=pv.second;
			if(odd)
			{
				if(dis[id][last][odd]+(last+w)*(last+w)<dis[v][w][0])
				{
					dis[v][w][0]=dis[id][last][odd]+(last+w)*(last+w);
					qn.push(Node(dis[v][w][0],w,0,v));
				}
			}
			else
			{
				if(dis[id][last][odd]<dis[v][w][odd^1])
				{
					dis[v][w][odd^1]=dis[id][last][odd];
					qn.push(Node(dis[v][w][odd^1],w,1,v));
				}
			}
		}
	}
}
void solve()
{
	n=read(),m=read();
	f(i,1,m) 
	{
		ll x=read(),y=read(),z=read();
		eg[x].pb(mp(y,z));
		eg[y].pb(mp(x,z));
	}
	dij();
	f(i,2,n)
	{
		ans[i]=INF;
		f(j,1,50) ans[i]=min(ans[i],dis[i][j][0]); 
	}
	f(i,1,n) printf("%lld ",(ans[i]==INF)?(-1):ans[i]);
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;
} 

codeforces1426F Number of Subsequences

题意:给一串由'a','b','c','?'组成的字符串,k可以变成成'a','b','c',若有k个'?',则在所有的(2^k)个字符串中,"abc"子串的数量和。
题解:假设这道题没有'?',则可以这样做,另dp[i][0/1/2]分别代表到第i个字符,"a","ab","abc"子串的数量,则有下列递推式

  if(str[i]=='a') 
  {
    dp[i][0]=dp[i-1][0]+1;
    dp[i][1]=dp[i-1][1];
    dp[i][2]=dp[i-1][2];
  }
  else if(str[i]=='b')
  {
    dp[i][0]=dp[i-1][0];
    dp[i][1]=dp[i-1][1]+dp[i-1][0];
    dp[i][2]=dp[i-1][2];
  }
  else
  {
    dp[i][0]=dp[i-1][0];
    dp[i][1]=dp[i-1][1];
    dp[i][2]=dp[i-1][2]+dp[i-1][1];
  }

现在有了'?',必须增加一个状态,dp[i][0/1/2/3]分别代表到第i个字符,序列的数量,"a"的数量,"ab"的数量,"abc"的数量,则有

//(有滚动数组优化)
if(ch=='a')  dp[1]=(dp[0]+dp[1])%Mod;
else if(ch=='b')  dp[2]=(dp[2]+dp[1])%Mod;
else if(ch=='c')  dp[3]=(dp[3]+dp[2])%Mod;
else
{
	dp[3]=(3*dp[3]+dp[2])%Mod;
	dp[2]=(3*dp[2]+dp[1])%Mod;
	dp[1]=(3*dp[1]+dp[0])%Mod;
	dp[0]=(3*dp[0])%Mod;
}

AC代码如下

#include<vector>
#include<queue>
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define f(i,a,b) for(register int i=a;i<=b;++i)
#define ll long long
inline ll read(){ll x;scanf("%lld",&x);return x;}
const int N = 100005;
const ll Mod = 1e9+7;
ll dp[4];
string str;
void solve()
{
	ll n=read();
	cin>>str;
	dp[0]=1ll;
	f(i,1,n)
	{
		char ch=str[i-1];
		if(ch=='a')	dp[1]=(dp[0]+dp[1])%Mod;
		else if(ch=='b')	dp[2]=(dp[2]+dp[1])%Mod;
		else if(ch=='c')	dp[3]=(dp[3]+dp[2])%Mod;
		else
		{
			dp[3]=(3*dp[3]+dp[2])%Mod;
			dp[2]=(3*dp[2]+dp[1])%Mod;
			dp[1]=(3*dp[1]+dp[0])%Mod;
			dp[0]=(3*dp[0])%Mod;
		}
	}
	cout<<dp[3];
}
int main()
{
	int T=1;
	while(T--) solve();
	return 0;
} 
原文地址:https://www.cnblogs.com/Tiork/p/14231190.html