dp动态规划刷题总结

引入

LIS

  1. 子序列(subsequence): 一个特定序列的子序列就是将给定序列中零个或多个元素去掉后得到的结果(不改变元素间相对次序)。
  2. 公共子序列(common subsequence): 给定序列X和Y,序列Z是X的子序列,也是Y的子序列,则Z是X和Y的公共子序列。
    dp动态规划刷题总结_第1张图片

基本写法

#include 
#define ll long long 
using namespace std;

int n,ans;
int a[10005],f[10005];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++){
		for(int j=1;j<i;j++){
			if(a[j]<a[i]) f[i]=max(f[i],f[j]);
		}
		f[i]++;
		ans=max(ans,f[i]);
	}
	cout<<ans<<endl;
	return 0;
}



贪心加二分

我们肯定要知道我们当前阶段最后一个元素为多少,还有当前我们的序列有多长。
前两种方法都是用前者做状态,我们为什么不可以用后做状态呢?
设 F[i]表示长度为 i 的最长上升子序列的末尾元素的最小值,我们发现这个数组的权值一定单调不降(仔细想一想,这就是我们贪心的来由)。于是我们按顺序枚举数组 A ,每一次对 F 数组二分查找,找到小于 A[i] 的最大的 F[j] ,并用它将 F[j+1]更新。

#include 
#define ll long long 
using namespace std;
const int N=200005;
int n;
int a[N];
int f[N];

int main(){
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	int ans=1; f[1]=a[1];
	for(int i=2;i<=n;++i){
		int l=1,r=ans,mid;
		while(l<=r){
			mid=(l+r)>>1;
			if(a[i]<=f[mid])r=mid-1;
			else l=mid+1;
		}f[l]=a[i];
		if(l>ans)++ans;
	}cout<<ans<<endl;
	return 0;
}

LCS

LCS问题具有最优子结构
两个序列的LCS问题包含两个序列的前缀的LCS,因此,LCS问题具有最优子结构性质。在设计递归算法时,不难看出递归算法具有子问题重叠的性质。
  设C[i,j]表示Xi和Yj的最长公共子序列LCS的长度。如果i=0或j=0,即一个序列长度为0时,那么LCS的长度为0。根据LCS问题的最优子结构性质,可得如下公式:
  dp动态规划刷题总结_第2张图片
dp动态规划刷题总结_第3张图片

#include 
#include 
#define MAXLEN 50

void LCSLength(char *x, char *y, int m, int n, int c[][MAXLEN], int b[][MAXLEN])
{
    int i, j;

    for(i = 0; i <= m; i++)
        c[i][0] = 0;
    for(j = 1; j <= n; j++)
        c[0][j] = 0;
    for(i = 1; i<= m; i++)
    {
        for(j = 1; j <= n; j++)
        {
            if(x[i-1] == y[j-1])
            {
                c[i][j] = c[i-1][j-1] + 1;
                b[i][j] = 1;                   
            }                                  
            else if(c[i-1][j] >= c[i][j-1])
            {
                c[i][j] = c[i-1][j];
                b[i][j] = 3;
            }
            else
            {
                c[i][j] = c[i][j-1];
                b[i][j] = 2;
            }
        }
    }
}

void PrintLCS(int b[][MAXLEN], char *x, int i, int j)
{
    if(i == 0 || j == 0)
        return;
    if(b[i][j] == 1)
    {
        PrintLCS(b, x, i-1, j-1);
        printf("%c ", x[i-1]);
    }
    else if(b[i][j] == 3)
        PrintLCS(b, x, i-1, j);
    else
        PrintLCS(b, x, i, j-1);
}

int main()
{
    char x[MAXLEN] = {"ABCBDAB"};
    char y[MAXLEN] = {"BDCABA"};

    int  b[MAXLEN][MAXLEN];                 
    int  c[MAXLEN][MAXLEN];

    int m, n;

    m = strlen(x);
    n = strlen(y);

    LCSLength(x, y, m, n, c, b);
    PrintLCS(b, x, m, n);

    return 0;
}

最长公共上升子序列

dp动态规划刷题总结_第4张图片


闫式分析法求解
dp动态规划刷题总结_第5张图片

#include

using namespace std;

const int N = 3010;

int n;
int a[N], b[N];
int f[N][N];

int main()
{
    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 ++ )
    {
        int maxv = 1;
        for (int j = 1; j <= n; j ++ )
        {
            f[i][j] = f[i - 1][j];//集合左半部分
            if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
            if (a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);//集合右半部分
        }
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);//最长的情况会出现在以B【n】结尾的状况中,取其中的最大值
    cout<<res;

    return 0;
}


dp问题

AcWing 271. 杨老师的照相排列

dp动态规划刷题总结_第6张图片
dp动态规划刷题总结_第7张图片

核心性质:

从高到低依次安排每个同学的位置,那么在安排过程中,当前同学一定占据每排最靠左的连续若干个位置,且从后往前每排人数单调递减。否则一定不满足“每排从左到右身高递减,从后到前身高也递减”这个要求。

DP的核心思想是用集合来表示一类方案,然后从集合的维度来考虑状态之间的递推关系。

受上述性质启发,状态表示为:

f[a][b][c][d][e]代表从后往前每排人数分别为a, b, c, d, e的所有方案的集合, 其中a >= b >= c >= d >= e;
f[a][b][c][d][e]的值是该集合中元素的数量;
状态计算对应集合的划分,令最后一个同学被安排在哪一排作为划分依据,可以将f[a][b][c][d][e]划分成5个不重不漏的子集:

当a > 0 && a - 1 >= b时,最后一个同学可能被安排在第1排,方案数是f[a - 1][b][c][d][e];
当b > 0 && b - 1 >= c时,最后一个同学可能被安排在第2排,方案数是f[a][b - 1][c][d][e];
当c > 0 && c - 1 >= d时,最后一个同学可能被安排在第3排,方案数是f[a][b][c - 1][d][e];
当d > 0 && d - 1 >= e时,最后一个同学可能被安排在第4排,方案数是f[a][b][c][d - 1][e];
当e > 0时,最后一个同学可能被安排在第5排,方案数是f[a][b][c][d][e - 1]

运用Y式dp分析法,利用集合思想求解
dp动态规划刷题总结_第8张图片

#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 31;

int n;
LL f[N][N][N][N][N];

int main()
{
    while (cin >> n, n)
    {
        int s[5] = {0};
        for (int i = 0; i < n; i ++ ) cin >> s[i];
        memset(f, 0, sizeof f);
        f[0][0][0][0][0] = 1;

        for (int a = 0; a <= s[0]; a ++ )
            for (int b = 0; b <= min(a, s[1]); b ++ )
                for (int c = 0; c <= min(b, s[2]); c ++ )
                    for (int d = 0; d <= min(c, s[3]); d ++ )
                        for (int e = 0; e <= min(d, s[4]); e ++ )
                        {
                            LL &x = f[a][b][c][d][e];
                            if (a && a - 1 >= b) x += f[a - 1][b][c][d][e];//映射,将每个这样的同学去掉
                            if (b && b - 1 >= c) x += f[a][b - 1][c][d][e];
                            if (c && c - 1 >= d) x += f[a][b][c - 1][d][e];
                            if (d && d - 1 >= e) x += f[a][b][c][d - 1][e];
                            if (e) x += f[a][b][c][d][e - 1];
                        }
        cout << f[s[0]][s[1]][s[2]][s[3]][s[4]] << endl;
    }

    return 0;
}


AcWing 275.传纸条

链接
dp动态规划刷题总结_第9张图片

解法一:无优化法

#include
using namespace std;
const int N=55;
int n,m;
int w[N][N];
int f[N][N][N][N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>w[i][j];
		for(int x1=1;x1<=n;x1++)
			for(int y1=1;y1<=m;y1++)
				for(int x2=1;x2<=n;x2++)
					for(int y2=1;y2<=m;y2++){
						int t=w[x1][y1];
						if(x1!=x2) t+=w[x2][y2];
								f[x1][y1][x2][y2]=max(f[x1][y1][x2][y2],f[x1-1][y1][x2-1][y2]+t);
								f[x1][y1][x2][y2]=max(f[x1][y1][x2][y2],f[x1-1][y1][x2][y2-1]+t);
								f[x1][y1][x2][y2]=max(f[x1][y1][x2][y2],f[x1][y1-1][x2-1][y2]+t);
								f[x1][y1][x2][y2]=max(f[x1][y1][x2][y2],f[x1][y1-1][x2][y2-1]+t);		
					}

				
			
	
	cout<<f[n][m][n][m];
	return 0;
	
} 

解法二:解法一的优化

首先考虑路径有交集该如何处理。
可以发现交集中的格子一定在每条路径的相同步数处。因此可以让两个人同时从起点出发,每次同时走一步,这样路径中相交的格子一定在同一步内。

状态表示:f[k, i, j] 表示两个人同时走了k步,第一个人在 (i, k - i) 处,第二个人在 (j, k - j)处的所有走法的最大分值。

状态计算:按照最后一步两个人的走法分成四种情况:

两个人同时向右走,最大分值是 f[k - 1, i, j] + score(k, i, j); 第一个人向右走,第二个人向下走,最大分值是f[k - 1, i, j - 1] + score(k, i, j); 第一个人向下走,第二个人向右走,最大分值是 f[k - 1, i- 1, j] + score(k, i, j); 两个人同时向下走,最大分值是 f[k - 1, i - 1, j - 1] + score(k, i, j); 注意两个人不能走到相同格子,即i和j不能相等。

#include
using namespace std;
const int N=55;
int n,m;
int w[N][N];
int f[N*2][N][N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>w[i][j];
	for(int k=1;k<=n+m;k++)
		for(int x1=max(1,k-m);x1<=min(k-1,n);x1++)
			for(int x2=max(1,k-m);x2<=min(k-1,n);x2++){
				int t=w[x1][k-x1];
				if(x1!=x2) t+=w[x2][k-x2];
				for(int a=0;a<=1;a++)
					for(int b=0;b<=1;b++){
						f[k][x1][x2]=max(f[k][x1][x2],f[k-1][x1-a][x2-b]+t);
					} 
			}
	
	cout<<f[n+m][n][n];
	return 0;
	
} 

背包问题

01背包

采用倒序状态时,对应每个物品唯一且只能使用一次

#include
using namespace std;
const int N=10010;
int f[N],v[N],w[N],ans;
int n,m;

int main(){
	cin>>n>>m;
	memset(f,0xcf,sizeof f); //0xcf = -INF
	f[0]=0;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];

	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]);
	}
	for(int j=0;j<=m;j++) ans=max(ans,f[j]);
	cout<<ans<<endl;
	return 0;	
}

数字组合

给定 N 个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。

#include
using namespace std;
const int N=10010;
int f[N];
int n,m;

int main(){
	cin>>n>>m;
	f[0]=1;
	while(n--){
		int v;
		cin>>v;
		for(int i=m;i>=v;i--) f[i]+=f[i-v];
	}
	cout<<f[m]<<endl;
	return 0;	
}

采药

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。
为此,他想拜附近最有威望的医师为师。
医师为了判断他的资质,给他出了一个难题。
医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
输入文件的第一行有两个整数T和M,用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。
接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出文件包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
数据范围
1≤T≤1000
1≤T≤1000
,
1≤M≤100
1≤M≤100
输入样例:

70 3
71 100
69 1
1 2

输出样例:

3

采药总时间相当于背包容量,每一株药相当于一件物品,采药时间相当于该物品的体积,草药的价值相当于物品价值。
时间复杂度
01背包问题的时间复杂度等于 物品数量 × 背包容量,因此本题的时间复杂度是 O(nm)O(nm)。

#include
using namespace std;
const int N=1010;
int n,t;
int f[N];
int main()
{
	cin>>t>>m;
	for(int i=0;i<m;i++)
	{
		int v,w;
		cin>>v>>w;
		for(int j=t;j>=v;j--)
		{
			f[j]=max(f[j],f[j-v]+w);
		}
	}
	cout<<f[t]<<endl;
 } 

装箱问题

有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积(正整数)。
要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
输入格式
第一行是一个整数 V,表示箱子容量。
第二行是一个整数 n,表示物品数。
接下来 n 行,每行一个正整数(不超过10000),分别表示这 n 个物品的各自体积。
输出格式
一个整数,表示箱子剩余空间。
数据范围
0 0 ,
0 0 输入样例:

24
6
8
3
12
7
9
7

输出样例:

0

最小剩余体积等于体积减去01背包所求能装的最大体积
此题的物品价值和所耗费体力均为物品的体积

#include
using namespace std;
const int N=20010;
int n,v;
int f[N];
int main()
{
	cin>>v>>n;
	for(int i=0;i<n;i++)
	{
		int w;
		cin>>w;
		for(int j=v;j>=w;j--)
		{
			f[j]=max(f[j],f[j-w]+w);
		}
	}
	cout<<v-f[v]<<endl;
 } 

宠物小精灵之收服

宠物小精灵是一部讲述小智和他的搭档皮卡丘一起冒险的故事。
一天,小智和皮卡丘来到了小精灵狩猎场,里面有很多珍贵的野生宠物小精灵。
小智也想收服其中的一些小精灵。
然而,野生的小精灵并不那么容易被收服。
对于每一个野生小精灵而言,小智可能需要使用很多个精灵球才能收服它,而在收服过程中,野生小精灵也会对皮卡丘造成一定的伤害(从而减少皮卡丘的体力)。
当皮卡丘的体力小于等于0时,小智就必须结束狩猎(因为他需要给皮卡丘疗伤),而使得皮卡丘体力小于等于0的野生小精灵也不会被小智收服。
当小智的精灵球用完时,狩猎也宣告结束。
我们假设小智遇到野生小精灵时有两个选择:收服它,或者离开它。
如果小智选择了收服,那么一定会扔出能够收服该小精灵的精灵球,而皮卡丘也一定会受到相应的伤害;如果选择离开它,那么小智不会损失精灵球,皮卡丘也不会损失体力。
小智的目标有两个:主要目标是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因为他们还要继续冒险。
现在已知小智的精灵球数量和皮卡丘的初始体力,已知每一个小精灵需要的用于收服的精灵球数目和它在被收服过程中会对皮卡丘造成的伤害数目。
请问,小智该如何选择收服哪些小精灵以达到他的目标呢?
输入格式
输入数据的第一行包含三个整数:N,M,K,分别代表小智的精灵球数量、皮卡丘初始的体力值、野生小精灵的数量。
之后的K行,每一行代表一个野生小精灵,包括两个整数:收服该小精灵需要的精灵球的数量,以及收服过程中对皮卡丘造成的伤害。
输出格式
输出为一行,包含两个整数:C,R,分别表示最多收服C个小精灵,以及收服C个小精灵时皮卡丘的剩余体力值最多为R。
数据范围
0 0 0

输入样例1:
10 100 5

7 10
2 40
2 50
1 20
4 20

输出样例1:

3 30

输入样例2:

10 100 5
8 110
12 10
20 10
5 200
1 110

输出样例2:

0 100
#include 
#include 

using namespace std;

const int N = 1010, M = 510;

int n, V1, V2;
int f[N][M];

int main()
{
    cin >> V1 >> V2 >> n;
    for (int i = 0; i < n; i ++ )
    {
        int v1, v2;
        cin >> v1 >> v2;
        for (int j = V1; j >= v1; j -- )
            for (int k = V2 - 1; k >= v2; k -- )//皮卡丘体力不小于0 
                f[j][k] = max(f[j][k], f[j - v1][k - v2] + 1);
    }

    cout << f[V1][V2 - 1] << ' ';
    int k = V2 - 1;
    while (k > 0 && f[V1][k - 1] == f[V1][V2 - 1]) k -- ;//找到最小第二维和最小价值相等 
    cout << V2 - k << endl;//k为消耗体力值最小 

    return 0;
}

完全背包

采用正序状态时对应每个物品可以使用无限次

#include
using namespace std;
const int N=10010;
int f[N],v[N],w[N],ans;
int n,m;

int main(){
	cin>>n>>m;
	memset(f,0xcf,sizeof f); //0xcf = -INF
	f[0]=0;
	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]);
	}
	for(int j=0;j<=m;j++) ans=max(ans,f[j]);
	cout<<ans<<endl;
	return 0;	
}

自然数拆分

给定一个自然数 N,要求把 N 拆分成若干个正整数相加的形式,参与加法运算的数可以重复。

注意:

拆分方案不考虑顺序;
至少拆分成 2 个数的和。
求拆分的方案数 mod2147483648 的结果。

#include
using namespace std;
typedef long long LL;
const int N=10010;
const LL mod=2147483648LL;
LL f[N],ans;
int n;

int main(){
	cin>>n;
	memset(f,0,sizeof f); 
	f[0]=1;

	for(int i=1;i<=n;i++){
		for(int j=i;j<=n;j++) f[j]=(f[j]+f[j-i])%mod;
	}
	ans=(f[n]>0 ? f[n]-1 : mod);
	cout<<ans<<endl;
	return 0;	
}

AcWing 280.陪审团

dp动态规划刷题总结_第10张图片

#include
using namespace std;
const int N=210,M=810,base=400;
//base为偏移量,将区间由【-400,400】化为【0,800】

int f[N][21][M];//f[i][j][k]从前i个人中选j个,差值为k
int p[N],d[N];
int n,m;
int ans[N];
int main(){
	int tot=1;
	while(scanf("%d%d",&n,&m),n||m){
		for(int i=1;i<=n;i++) scanf("%d%d",&p[i],&d[i]);
		memset(f,-0x3f,sizeof f);
		f[0][0][base]=0;
		
		for(int i=1;i<=n;i++)
			for(int j=0;j<=m;j++)
				for(int k=0;k<M;k++){
					f[i][j][k]=f[i-1][j][k];
					int t=k-(p[i]-d[i]);
					if(t<0||t>800) continue;//判断i是否可选
					if(j<1) continue;
					else f[i][j][k]=max(f[i][j][k],f[i-1][j-1][t]+p[i]+d[i]);
				} 
		int v=0;
		//判断选正的还是负的
		while(f[n][m][base-v]<0&&f[n][m][base+v]<0) v++;
		if(f[n][m][base-v]>f[n][m][base+v] ) v=base-v;
		else v=v+base;
		//从终点倒退
		int i=n,j=m,k=v;
		int cnt=0;
		while(j)
		{
			if(f[i][j][k]==f[i-1][j][k]) i--;
			else {
				ans[cnt++]=i;
				k-=p[i]-d[i];
				i--,j--;
			}
		}
		int sp=0,sd=0;
		for(int i=0;i<cnt;i++){
			sp+=p[ans[i]];
			sd+=d[ans[i]];
		}
		printf("Jury #%d\n",tot++);
		printf("Best jury has value %d for prosecution and value %d for defence:\n",sp,sd);
		for(int i=cnt-1;i>=0;i--)
		printf(" %d",ans[i]);
		puts("\n");
		
	}
	return 0;
} 

区间dp

AcWing 282.石子合并

设有 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

#include
using namespace std;
typedef long long LL;
const int N=310;
int f[N][N];
int s[N];
int n;
int main(){
	 cin>>n;
	 for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];//前缀和
	 //遍历区间长度
	 for(int len=2;len<=n;len++){
	 	for(int i=1;i+len-1<=n;i++){//左端点
	 		int j=i+len-1;//右端点
	 		f[i][j]=0x3f3f3f3f;
	 		for(int k=i;k<j;k++){
	 			f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
			 }
		 }
	 }
	 cout<<f[1][n];
	 return 0;
} 

AcWing 283.多边形

“多边形游戏”是一款单人益智游戏。

游戏开始时,给定玩家一个具有 N 个顶点 N 条边(编号 1∼N)的多边形,如图 1 所示,其中 N=4。

每个顶点上写有一个整数,每个边上标有一个运算符 +(加号)或运算符 *(乘号)。

dp动态规划刷题总结_第11张图片

第一步,玩家选择一条边,将它删除。

接下来在进行 N−1 步,在每一步中,玩家选择一条边,把这条边以及该边连接的两个顶点用一个新的顶点代替,新顶点上的整数值等于删去的两个顶点上的数按照删去的边上标有的符号进行计算得到的结果。

下面是用图 1 给出的四边形进行游戏的全过程。

dp动态规划刷题总结_第12张图片

最终,游戏仅剩一个顶点,顶点上的数值就是玩家的得分,上图玩家得分为 0。

请计算对于给定的 N 边形,玩家最高能获得多少分,以及第一步有哪些策略可以使玩家获得最高得分。

输入格式
输入包含两行,第一行为整数 N。

第二行用来描述多边形所有边上的符号以及所有顶点上的整数,从编号为 1 的边开始,边、点、边…按顺序描述。

其中描述顶点即为输入顶点上的整数,描述边即为输入边上的符号,其中加号用 t 表示,乘号用 x 表示。

输出格式
输出包含两行,第一行输出最高分数。

在第二行,将满足得到最高分数的情况下,所有的可以在第一步删除的边的编号从小到大输出,数据之间用空格隔开。

数据范围
3≤N≤50,
数据保证无论玩家如何操作,顶点上的数值均在 [−32768,32767] 之内。

#include

using namespace std;

const int N = 110, INF = 32768;

int n;
int w[N];
char c[N];
int f[N][N], g[N][N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> c[i] >> w[i];
        c[i + n] = c[i];
        w[i + n] = w[i];
    }

    for (int len = 1; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n * 2; l ++ )
        {
            int r = l + len - 1;
            if (len == 1) f[l][r] = g[l][r] = w[l];
            else
            {
                f[l][r] = -INF, g[l][r] = INF;
                for (int k = l; k < r; k ++ )
                {
                    char op = c[k + 1];
                    int minl = g[l][k], minr = g[k + 1][r];
                    int maxl = f[l][k], maxr = f[k + 1][r];
                    if (op == 't')
                    {
                        f[l][r] = max(f[l][r], maxl + maxr);
                        g[l][r] = min(g[l][r], minl + minr);
                    }
                    else
                    {
                    	//答案只能从这四种情况中选取
                        int x1 = maxl * maxr, x2 = maxl * minr, x3 = minl * maxr, x4 = minl * minr;
                        f[l][r] = max(f[l][r], max(max(x1, x2), max(x3, x4)));
                        g[l][r] = min(g[l][r], min(min(x1, x2), min(x3, x4)));
                    }
                }
            }
        }

    int res = -INF;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[i][i + n - 1]);
    cout << res << endl;

    for (int i = 1; i <= n; i ++ )
        if (res == f[i][i + n - 1])
            cout << i << ' ';

    return 0;
}

树形dp

AcWing 285.没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

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

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

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

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

输入格式
第一行一个整数 N。

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

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

输出格式
输出最大的快乐指数。

数据范围
1≤N≤6000,
−128≤Hi≤127

#include
using namespace std;
const int N=6010;

int n,f[N][2],w[N];
int head[N],e[N],Next[N],tot;
bool st[N];
void add(int a,int b){
    e[tot]=b,Next[tot]=head[a],head[a]=tot++;
}
void dfs(int u){
    f[u][1]=w[u];
    for(int i=head[u];~i;i=Next[i]){
        int j=e[i];
        dfs(j);
        f[u][0]+=max(f[j][0],f[j][1]);//不选u
        f[u][1]+=f[j][0];//选择u
    }
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>w[i];
    memset(head,-1,sizeof head);//不可少
    for(int i=0;i<n-1;i++){
        int a,b;
        cin>>a>>b;
        add(b,a);
        st[a]=true;
    }
    int root=1;
    while(st[root]) root++;//根节点
    dfs(root);
    cout<<max(f[root][0],f[root][1])<<endl;
    return 0;
 } 


AcWing 286.选课

树形分组背包

学校实行学分制。

每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。

学校开设了 N 门的选修课程,每个学生可选课程的数量 M 是给定的。

学生选修了这 M 门课并考核通过就能获得相应的学分。

在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程的基础上才能选修。

例如《Windows程序设计》必须在选修了《Windows操作基础》之后才能选修。

我们称《Windows操作基础》是《Windows程序设计》的先修课。

每门课的直接先修课最多只有一门。

两门课可能存在相同的先修课。

你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修条件。

假定课程之间不存在时间上的冲突。

输入格式
输入文件的第一行包括两个整数 N、M(中间用一个空格隔开)其中 1≤N≤300,1≤M≤N。

接下来 N 行每行代表一门课,课号依次为 1,2,…,N。

每行有两个数(用一个空格隔开),第一个数为这门课先修课的课号(若不存在先修课则该项为 0),第二个数为这门课的学分。

学分是不超过 10 的正整数。

输出格式
输出一个整数,表示学分总数。

#include
using namespace std;
const int N=310;

int n,m,f[N][N],w[N];//f[i][m] 在以i为节点的树中选体积为m的集合

int head[N],e[N],Next[N],tot;
void add(int a,int b){
	e[++tot]=b,Next[tot]=head[a],head[a]=tot;
}
void dfs(int u){
	for(int i=head[u];i;i=Next[i]){//遍历物品组 
		int son=e[i];
		dfs(son);
		for(int j=m;j>=1;j--){//从大到小遍历体积 
                for(int k=1;k<=j;k++){//遍历决策 
                    f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);//分组背包
                }
            }
	}
	for(int i=m;i;i--) f[u][i]=f[u][i-1]+w[u];
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int a;
		cin>>a>>w[i];
		add(a,i);
	}
	m++;//假设森林中的树的根节点都连接点0作为虚拟根节点 
	dfs(0);
	cout<<f[0][m]<<endl;
	return 0;
 } 

环形与后效性处理

法一:分情况讨论

AcWing 288. 休息时间

在某个星球上,一天由 N 个小时构成,我们称 0 点到 1 点为第 1 个小时、1 点到 2 点为第 2 个小时,以此类推。

在第 i 个小时睡觉能够恢复 Ui 点体力。

在这个星球上住着一头牛,它每天要休息 B 个小时。

它休息的这 B 个小时不一定连续,可以分成若干段,但是在每段的第一个小时,它需要从清醒逐渐入睡,不能恢复体力,从下一个小时开始才能睡着。

为了身体健康,这头牛希望遵循生物钟,每天采用相同的睡觉计划。

另外,因为时间是连续的,即每一天的第 N 个小时和下一天的第 1 个小时是相连的(N 点等于 0 点),这头牛只需要在每 N 个小时内休息够 B 个小时就可以了。

请你帮忙给这头牛安排一个睡觉计划,使它每天恢复的体力最多。

输入格式
第 1 行输入两个空格隔开的整数 N 和 B。

第 2…N+1 行,第 i+1 行包含一个整数 Ui。

输出格式
输出一个整数,表示恢复的体力值。

数据范围
3≤N≤3830
2≤B 0≤Ui≤200000

#include
using namespace std;
const int N=4000,INF=0x3f3f3f3f;

int n,m,f[2][N][2],w[N];
//f[i][j][0],前i小时休息了j小时,且第i小时正在休息,累计回复最大值 
//f[i][j][1],前i小时休息了j小时,且第i小时没有在休息,累计回复最大值 
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>w[i];
	//第n小时在睡觉 
	memset(f,-INF,sizeof f);
	f[1][0][0]=0,f[1][1][1]=0;
	for(int i=2;i<=n;i++)
	 	for(int j=0;j<=m;j++){
	 	//不轮转爆内存了
	 		f[i&1][j][0]=max(f[i-1&1][j][0],f[i-1&1][j][1]);
	 		f[i & 1][j][1] = -INF;
	 		if(j) f[i&1][j][1]=max(f[i-1&1][j-1][0],f[i-1&1][j-1][1]+w[i]);
		 }
	int ans=max(f[n&1][m][0],f[n&1][m][1]);
	//第n小时没在睡觉 
	memset(f,-INF,sizeof f);
	f[1][1][1]=w[1],f[1][0][0]=0;
	for(int i=2;i<=n;i++)
	 	for(int j=0;j<=m;j++){
	 		f[i&1][j][0]=max(f[i-1&1][j][0],f[i-1&1][j][1]);
	 		f[i & 1][j][1] = -INF;
	 		if(j) f[i&1][j][1]=max(f[i-1&1][j-1][0],f[i-1&1][j-1][1]+w[i]);
		 }
	ans=max(ans,f[n&1][m][1]);
	cout<<ans<<endl;
	return 0;
 } 
 

法二:破环成链(复制一倍在末尾)*

AcWing 289.环路运输

在一条环形公路旁均匀地分布着 N 座仓库,编号为 1∼N,编号为 i 的仓库与编号为 j 的仓库之间的距离定义为 dist(i,j)=min(|i−j|,N−|i−j|),也就是逆时针或顺时针从 i 到 j 中较近的一种。

每座仓库都存有货物,其中编号为 i 的仓库库存量为 Ai。

在 i 和 j 两座仓库之间运送货物需要的代价为 Ai+Aj+dist(i,j)。

求在哪两座仓库之间运送货物需要的代价最大。

输入格式
第一行包含一个整数 N。

第二行包含 N 个整数 A1∼AN。

输出格式
输出一个整数,表示最大代价。

数据范围
1≤N≤106,
1≤Ai≤107
输入样例:

5 1 8 6 2 5

输出样例:

15

类似于滑动窗口,用单调队列维护

#include 
using namespace std;

const int N=2000010;
deque<int> q;//双端队列
int s[N],ans=-0x3f3f3f3f;
int n,m;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++){
		scanf("%d",&s[i]);
		s[i+n]=s[i];
	}
	int len=n/2;
	q.push_back(0);
	for(int i=1;i<=n*2;i++){
		while(q.size()&&i-q.front()>len) q.pop_front();
		ans=max(ans,i-q.front()+s[q.front()]+s[i]);
		while(q.size()&&s[q.back()]-q.back()<=s[i]-i) q.pop_back();
		q.push_back(i);
		
	}
	cout<<ans<<endl;
	return 0;
}
 

状态压缩dp

AcWing 91. 最短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

#include
using namespace std;
const int N=21,M=1<<20;

int f[M][N],w[N][N];

int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++) cin>>w[i][j];
		
	memset(f,0x3f,sizeof f);
	f[1][0]=0;
	for(int i=0;i<1<<n;i++)
		for(int j=0;j<n;j++){
			if(i>>j&&1){//如果i集合中第j位是1,也就是到达过这个点
				for(int k=0;k<n;k++){//枚举到达j的点k
					if(i^(1<<j) >>k &1)//第k位是否为1 
						f[i][j]=min(f[i][j],f[i^(1<<j)][k]+w[k][j]);
				}
			}
		}
	cout<<f[(1<<n)-1][n-1];
	return 0;
 } 

AcWing 291. 蒙德里安的梦想

求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。

例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

如下图所示:

在这里插入图片描述

输入格式
输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N 和 M。

当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。

输出格式
每个测试用例输出一个结果,每个结果占一行。

数据范围
1≤N,M≤11

#include
#define mem(a,b) memset(a,b,sizeof a) 
using namespace std;
typedef long long ll;

const int N=12,M=1<<N;

int st[M];
ll f[N][M];

int main(){
	int n,m;
	while(cin>>n>>m,n||m)
	{
		for(int i=0;i< 1<<n;i++){
			int cnt=0;
			st[i]=true;
			for(int j=0;j<n;j++){
				if(i>>j & 1){
					if(cnt & 1) st[i]=false;
					cnt=0;
				}else cnt++;
			}
			if(cnt & 1) st[i]=false;
		}
		mem(f,0);
        f[0][0] = 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((k&j)==0 && (st[j|k])){
						f[i][j]+=f[i-1][k];
					}
				}
			}
		}
		cout<<f[m][0]<<endl;
	}
	return 0;
}
 

转载题解

#include
using namespace std;

const int N=12, M = 1<< N;  

long long f[N][M] ;// 第一维表示列, 第二维表示所有可能的状态

bool st[M];  //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。

//vector state[M];  //二维数组记录合法的状态
vector<vector<int>> state(M);  //两种写法等价:二维数组
int m , n;

int main(){

    while(cin>>n>>m, n||m){ //读入n和m,并且不是两个0即合法输入就继续读入

        //第一部分:预处理1
        //对于每种状态,先预处理每列不能有奇数个连续的0

        for(int i=0; i< 1<<n; i++){

            int cnt =0 ;//记录连续的0的个数

            bool isValid = true; // 某种状态没有奇数个连续的0则标记为true

            for(int j=0;j<n;j++){ //遍历这一列,从上到下

                 if( i>>j &1){  //i>>j位运算,表示i(i在此处是一种状态)的二进制数的第j位; &1为判断该位是否为1,如果为1进入if
                    if(cnt &1) { //这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
                        isValid =false;break;
                    } 
                    cnt=0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。//其实清不清零没有影响
                 }
                 else cnt++; //否则的话该位还是0,则统计连续0的计数器++。
            }
            if(cnt &1)  isValid =false; //最下面的那一段判断一下连续的0的个数

            st[i]  = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
        }

        //第二部分:预处理2
        // 经过上面每种状态 连续0的判断,已经筛掉一些状态。
        //下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突

        for(int j=0;j< 1<<n;j++){ //对于第i列的所有状态
            state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
            for(int k=0;k< 1<<n;k++){ //对于第i-1列所有状态
                if((j&k )==0 && st[ j| k] ) // 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行) 
                //解释一下st[j | k] 
                //已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
                //我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,还要考虑自己这一列(i-1列)横插到第i列的
                //比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
                //那么合在第i-1列,到底有多少个1呢?自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
                //这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的

                    state[j].push_back(k);  //二维数组state[j]表示第j行, 
                    //j表示 第i列“真正”可行的状态,如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
                    //“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
            }

        }

        //第三部分:dp开始

        memset(f,0,sizeof f);  //全部初始化为0,因为是连续读入,这里是一个清空操作。类似上面的state[j].clear()

        f[0][0]=1 ;// 这里需要回忆状态表示的定义,按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
        //首先,这里没有-1列,最少也是0列。其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。

        for(int i=1;i<= m;i++){ //遍历每一列:第i列合法范围是(0~m-1列)
            for(int j=0; j< 1<<n; j++){  //遍历当前列(第i列)所有状态j
                for( auto k : state[j])    // 遍历第i-1列的状态k,如果“真正”可行,就转移
                    f[i][j] += f[i-1][k];    // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
            }
        }

        //最后答案是什么呢?
        //f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
        //即整个棋盘处理完的方案数

        cout<< f[m][0]<<endl;





    }
}   

作者:lishizheng
链接:https://www.acwing.com/solution/content/28088/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

单调队列优化dp

AcWing 298. 围栏

dp动态规划刷题总结_第13张图片
类比AcWing 289 滑动窗口选最值

#include
using namespace std;

const int N=16010,M=110;

int n,m;
deque<int> q;
int f[M][N];

struct car{
	int l,s,p;
	bool operator <(const car& t) const 
	{
		return s<t.s; 
	}
}car[M];
int calc(int i,int k){
	return f[i-1][k]-car[i].p*k; 
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++) cin>>car[i].l>>car[i].p>>car[i].s;
	sort(car+1,car+1+m);
	for(int i=1;i<=m;i++)
	{
		for(int k=max(0,car[i].s-car[i].l); k <car[i].s;k++){
			//,插入新决策,维护队尾单调性 
			while(!q.empty() && calc(i,q.back()) <=calc(i,k)) q.pop_back();
			q.push_back(k);
		}
		for(int j=1;j<=n;j++){
			//不粉刷时的转移 
			f[i][j]=max(f[i-1][j],f[i][j-1]);
			//粉刷k+1 —j块木板 
			if(j>=car[i].s){
				//排除对头不合法决策 
				while(!q.empty() && q.front()<j-car[i].l) q.pop_front();//对头非空,取对头进行转移 
				if(!q.empty()) f[i][j]=max(f[i][j],car[i].p*j + calc(i,q.front()));
			}
		}
	}
	cout<<f[m][n]<<endl;
	return 0;
} 

斜率优化

AcWing 300. 任务安排1

dp动态规划刷题总结_第14张图片

#include
#define ll long long 
using namespace std;
const int N=5010,M=110;
int n,s;
int ans=1e9;
int sumt[N],sumc[N];
ll f[N];
int main(){
    cin>>n>>s;
    for(int i=1;i<=n;i++) {
        cin>>sumt[i]>>sumc[i];
        sumt[i]+=sumt[i-1];
        sumc[i]+=sumc[i-1];
    }
    memset(f,0x3f,sizeof f);
    f[0]=0;
    for(int i=1;i<=n;i++){
        for(int j=0;j<i;j++){
                f[i]=min(f[i],f[j]+sumt[i]*(sumc[i]-sumc[j]) + s*(sumc[n]-sumc[j]) );
            }

    }

    cout<<f[n]<<endl;
    return 0;
}

作者:拓海
链接:https://www.acwing.com/activity/content/code/content/1348723/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

301. 任务安排2

同上 数据范围增大
dp动态规划刷题总结_第15张图片

#include
#define ll long long
using namespace std;
const int N=300010;
ll n,s;
ll sumt[N],sumc[N];
ll f[N];
ll q[N]; 
int main(){
	cin>>n>>s;
	for(int i=1;i<=n;i++) {
		cin>>sumt[i]>>sumc[i];
		sumt[i]+=sumt[i-1];
		sumc[i]+=sumc[i-1];
	}
	memset(f,0x3f,sizeof f);
	f[0]=0;
	int l=1,r=1;
	q[1]=0;
	for(int i=1;i<=n;i++){
	 //检查对头的两个决策变量q[l] q[l+1] 两点间斜率< (s+sumt[i]) q[l]出队
		while(l<r && ( f[q[l+1]]-f[q[l]] ) <= (s+sumt[i]) * (sumc[q[l+1]]-sumc[q[l]]) ){
			l++;
		}
		//状态转移
		f[i]=f[q[l]] - ( (s+sumt[i]) * sumc[q[l]]) + sumt[i]*sumc[i] + s*sumc[n];
		//检查三个决策点是否满足斜率单增 不满足 q[r]出队
		while(l<r && (f[q[r]]-f[q[r-1]])*(sumc[i] - sumc[q[r]] ) >= (sumc[q[r]] - sumc[q[r-1]]) * (f[i]-f[q[r]])  ){
			r--;
		}
		//新决策插入对头
		q[++r]=i;
	}

	cout<<f[n]<<endl;
	return 0;
}

你可能感兴趣的:(ACM,算法)