动态规划题目记录

动态规划

子序列问题

最长上升子序列
最长公共序列

以上两个的详解可以看这里

最长公共上升子序列
#include<bits/stdc++.h>
using namespace std;
const int N = 3010;
int a[N],b[N];
int f[N][N];
int res;
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 ++)
	{
		int max_fb = 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],max_fb);
			if(b[j] < a[i] && f[i - 1][j] + 1 > max_fb)
				max_fb = f[i - 1][j] + 1;
		}
	}
	for(int i = 1;i <= n;i ++) res = max(res,f[n][i]);
	cout << res;
	return 0;
}

背包问题

背包问题的二维优化

题目:多重背包的二维优化

#include<bits/stdc++.h>
using namespace std;
const int N = 2020;

int n,m;
typedef pair<int ,int > PII;
vector<PII> good;
int f[N];
int main()
{
	cin >> n >> m;
	for(int i = 1;i <= n;i ++)
	{
		int v,w,s;
		cin >> v >> w >> s;
		for(int j = 1;j <= s;j <<= 1)
		{
			s -= j;//将s进行二进制分解
			good.push_back({v * j,w * j});//将分解后的作为一个新的物品
		}
		if(s) good.push_back({s * v,w * s});//不要忘了s可能剩下,其本身也应该作为一个物品
	}
	for(int i = 0;i < good.size();i ++)
		for(int j = m;j >= good[i].first;j --)
			f[j] = max(f[j],f[j - good[i].first] + good[i].second);
	cout << f[m];
	return 0;
}
背包的单调队列优化

题目看这里

#include<bits/stdc++.h>
using namespace std;
const int V = 20003;
int f[V];
int temp[V];
int q[V];
int n,m,v,w,s;


int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= n;i ++)
	{
		scanf("%d%d%d",&v,&w,&s);
		memcpy(temp,f,sizeof(f));//temp用来储存上一个状态
		for(int j = 0;j < v;j ++)//j枚举余数
		{
			int head = 0,tail = -1;
			for(int k = j;k <= m;k += v)//k枚举每个%v = j的数
			{
				f[k] = temp[k];
				if(head <= tail && (k - q[head]) / v > s) head ++;
				if(head <= tail) f[k] = max(f[k],temp[q[head]] + (k - q[head]) / v * w);
				while(head <= tail && temp[k] - (k - q[tail]) / v * w >= temp[q[tail]] )tail --;
				q[++ tail] = k;
			}
		}
	}
	cout << f[m];
	return 0;
}

简单的说一下思路:
每一次选择一个物品,我们都可以将0~m的体积变成许多个集合,集合的划分标准是以体积%物体体积划分
我们把余数相同的划分为同一个集合,不同集合之间在更新值的时候不会互相干扰
所以我们就将余数作为划分阶段
这里可以用单调队列优化,因为当枚举到体积k的时候,始终只能往前面取到k - s * c,所以用单调队列来维护这个区间的最大值
这里维护单调队列判断的时候,不能直接判断上一个状态下对应的值的大小,因为在后面的一般都比前面的值大

那么我们如何判断呢?(具体看代码)其实就是将当前将要加入的数对应的值减去 ( 当前的体积値 − t a i l ) / v ∗ w (当前的体积値 - tail) / v* w (当前的体积値tail)/vw,如果减去之后剩下的值仍然大于g[tail]那么就说明tail对应的一定不是最优解

因为我们需要比较的其实是
假设 x 为将要加入的元素 我们就要比较 t e p m [ x ] + ( k − x ) / v ∗ w 和 t e p m [ t a i l ] + ( k − t a i l ) / v ∗ w 用前一个减去后一个得到 t e m p [ x ] − t e m p [ t a i l ] − ( x − t a i l ) / v ∗ w 再化简就变成了上面的式子 假设x为将要加入的元素\\ 我们就要比较tepm[x] +(k-x)/v * w和 tepm[tail] +(k-tail)/v * w\\ 用前一个减去后一个得到temp[x]-temp[tail] -(x - tail) / v * w\\ 再化简就变成了上面的式子 假设x为将要加入的元素我们就要比较tepm[x]+(kx)/vwtepm[tail]+(ktail)/vw用前一个减去后一个得到temp[x]temp[tail](xtail)/vw再化简就变成了上面的式子

区间dp

合并石子

区间问题的关键就在于,选择区间的长度作为状态,左右区间的位置l,r作为阶段,在l~r这个区间中的的划分点k作为决策

比较简单就不写

多边形

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


const int N = 102;
const int MIN = -33786;
int f[N][N][2];//f[l][r][1]代表l~r这个区间内的最大值,f[l][r][0]代表l~r这个区间内的最小值
char s[N];//符号
int w[N];//数据

int main()
{
	int n;
	cin >> n;
	for(int i = 1;i <= n;i ++)
	{
		cin >> s[i] >> w[i];
		s[i + n] = s[i];
		w[i + n] = w[i];//将原来的复制一倍后接在后面(处理环形动态规划的常用方式)
	}
	for(int len = 1;len <= n;len ++)
	{//枚举长度,长度最多为n
		for(int l = 1;l <= n * 2 - len + 1;l ++)
		{//枚举左边界
			int r = l + len - 1;//右边界
			if(len == 1) f[l][r][0] = f[l][r][1] = w[l];//如果长度为1,最大值和最小值都只能是它本身
			else
			{
				f[l][r][0] = -MIN;
				f[l][r][1] = MIN;//赋初始值
				for(int k = l;k < r;k ++)
				{//枚举分界点
					if(s[k + 1] == 't')
					{
						f[l][r][1] = max(f[l][r][1],f[l][k][1] + f[k + 1][r][1]);
						f[l][r][0] = min(f[l][r][0],f[l][k][0] + f[k + 1][r][0]);
					}//如果是加号,最大值等于最大值相加最小值等于最小值相加
					else
					{
						int a1 = f[l][k][0] * f[k + 1][r][1],
							a2 = f[l][k][1] * f[k + 1][r][1],
							a3 = f[k + 1][r][0] * f[l][k][1],
							a4 = f[k + 1][r][0] * f[l][k][0];
						f[l][r][1] = max(f[l][r][1],max(max(a1,a2),max(a3,a4)));
						f[l][r][0] = min(f[l][r][0],min(min(a1,a2),min(a3,a4)));
					}//如果是乘号,就无脑判断一遍四种情况的大小,因为可能存在最小值与最小值相乘比较大(当两者都为负数的时候)
				}
			}
		}
	}
	int res = MIN;
	for(int l = 1;l <= n;l ++) res = max(f[l][l + n - 1][1],res);
	cout << res << endl;
	for(int l = 1;l <= n;l ++)
		if(f[l][l + n - 1][1] == res)
			cout << l << " ";
	return 0;
}

金字塔

这道题,如果简单的划分按照上面的方式划分的话,就有可能产生重复的情况,所以这道题在划分的时候,必须还要加一点限制条件,就是枚举k时,k~r必须只有一棵子树,剩下部分可以随意.

以及,一棵树的序列,必须是奇数,所以在下面某些循环的时候需要+2,并且一棵树的序列的开头和结尾必然相同

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 303,mod = 1e9;

int f[N][N];
string s;
int n;
int solve(int l,int r)
{
	if(s[l] != s[r]) return f[l][r] = 0;
	if(l == r) return f[l][r] = 1;
	if(f[l][r] != -1) return f[l][r];
	f[l][r] = 0;
	for(int k = l;k < r - 1;k += 2)
	{
		if(s[k + 1] == s[r - 1])
			f[l][r] = (f[l][r] + (ll)solve(l,k) * solve(k + 1,r - 1)) % mod;
	}
	return f[l][r];
}
int main()
{
	cin >> s;
	n = s.size();
	for(int i = 0;i <= n;i ++)
		for(int j = 0;j <= n;j ++)
			f[i][j] = -1;
	if(n % 2 == 0)
	{
		puts("0");
		return 0;
	}
	cout << solve(0,n - 1);
	return 0;
}

状态压缩DP

蒙德里安的梦想

状态压缩的典型模板题

首先进行分析,将每一行作为阶段,每一列的状态化为一个M位二进制数,我们用1代表这是一个还没有补充完整的竖着的长方条的上端,0代表其他情况

然后就有这样的性质:,从状态k转移到状态j,必须要满足,j和k按位与的结果必须是0,这样才能保证所有k状态中剩下半截的长方条能匹配成功.其次,j和k按位或的结果中,每一段连续的0的个数必须是偶数,这些0代表着若干个横着的长方形

然后就是可以在dp前预处理出 [ 0 , 2 M − 1 ] [0,2^M - 1] [0,2M1]中所有满足"二进制下表示每一段连续的0都有偶数个"

怎样处理的细节见代码

#include<bits/stdc++.h>
using namespace std;
int n,m;
long long f[12][1 << 11];
bool in_s[1 << 11];
int main()
{
	while(cin >> n >> m)
	{
		for(int i = 0;i < 1 << m;i ++)//预处理数,用i来枚举
		{
			bool cnt = 0,has_odd = 0;
			for(int j = 0;j < m;j ++)
				if(i >> j & 1) has_odd |= cnt;//手动模拟一些数可知,如果一旦出现了连续的奇数个零,那么cnt会变成1,在下一次遇到1的时候,has_odd就会变成1.
				else cnt ^= 1;
				in_s[i] = has_odd | cnt ? 0 : 1;//最后还要再判断一次,因为最后一位不一定出现1,不会进入上面的那个条件语句
		}//统计
		f[0][0] = 1;//第0行没有伸出任何竖着的长方条,只有一种方案
		for(int i = 1;i <= n;i ++)
			for(int j = 0;j < 1 << m;j ++)
			{
				f[i][j] = 0;
				for(int k = 0;k < 1 << m;k ++)
				{
					if((j & k) == 0 && in_s[j | k])
						f[i][j] += f[i - 1][k];//累加
				}
			}
		cout << f[n][0] << endl;
	}
	return 0;
}

互不侵犯

用这个当例题写一下压缩dp的一些处理技巧

题目就不多说了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 10;
ll f[N][100][82];
int n,m;
int state[100],cnt;
int c[100];

void lowbit(int x,int id)
{
	while(x)
	{
		x -= (x & -x);
		c[id] ++;
	}//计算一个数里面有多少个1,用lowbit运算会快一点
	return;
}
void address()
{
	for(int i = 0;i < (1 << n);i ++)
	{
		if((i & (i << 1)) == 0)
		state[++ cnt] = i;
	}//判断左右两边是否有1的存在,将原来的数向左向右移动一位,如果与上原来的数之后的值不为0,就说明,有相邻的1;
}
int main()
{
	cin >> n >> m;
	address();
	for(int i = 1;i <= cnt;i ++)
		lowbit(state[i],i);
	
	for(int i = 1;i <= cnt ;i ++) f[1][i][c[i]] = 1;
	//初始化第一排
	for(int i = 2;i <= n;i ++)
	{
		for(int j = 1;j <= cnt;j ++)
		{
			for(int k = 1;k <= cnt;k ++)
			{
				if((state[k] & (state[j] << 1)) == 0 && (state[k] & (state[j] >> 1)) == 0 && (state[k] & state[j]) == 0 )//判断是否合法
				{
					for(int x = c[j];x <= m;x ++)
					f[i][j][x] += f[i - 1][k][x - c[j]];
				}
			}
		}
	}
	ll res = 0;
	for(int i = 1;i <= cnt;i ++)
		res += f[n][i][m];//记得累加答案
	cout << res;
	return 0;
}

补充练习

低买

这道题其实是LIS问题加上输出方案数,比较麻烦的就是统计方案。首先按照题目的要求,要去重,而这一步可以在进行动规的时候写。去重的话,优先保留后面的数,因为如果两个数一样,那么很明显第二个数可能得到的长度更大。

#include<bits/stdc++.h>
using namespace std;
const int N = 5003;
int f[N];
int cnt[N];
int a[N];
int main()
{
	int n;
	cin >> n;
	for(int i = 1;i <= n;i ++)
		scanf("%d",&a[i]);
	int len_max = 0;
	for(int i = 1;i <= n;i ++)
	{
		
		f[i] = 1;//对于没一个数,它的长度至少为1
		for(int j = 1;j < i;j ++)
		{
			if(a[i] < a[j])
				f[i] = max(f[i],f[j] + 1);//状态转移
			else if(a[i] == a[j])
					f[j] = -10;//去重
		}
		len_max = max(len_max,f[i]);//记录最大长度
		if(f[i] == 1)
			cnt[i] = 1;//此处特判,为1说明满足以i为结尾的长度为1的方案数只有一种,而如果不是1的话,它自己本身就不能作为一种方案,而只能用前面合法的状态相加
		else for(int j = 1;j < i;j ++)
			if(f[i] == f[j] + 1 && a[j] > a[i]) cnt[i] += cnt[j];//要将前面合法的状态加起来
	}
	int ans = 0;
	for(int i = 1;i <= n;i ++) if(f[i] == len_max) ans += cnt[i];//有可能存在多个以不同的数结尾,但是长度都是最长的,所以此处还是要加一遍
	cout << len_max << " " << ans;
	return 0;
}

旅行

#include<bits/stdc++.h>
using namespace std;
const int N = 81;
string a,b;
int f[N][N];
int len;
int la,lb;
char ans[N];
void print()
{
	for(int i = 1;i<= len;i ++)
		cout << ans[i];
	puts("");
}
void dfs(int i,int j,int step)
{
//	cout << 1;
	if(step > len)
	{
		print();
		return;
	}
	if (a[i] == b[j])//如果当前搜到的两个数相等,那么它们就必须被选,因为如果不选这两个,后面的答案不可能更优
    {
        ans[step] = a[i];
        dfs(i + 1, j + 1, step + 1);//记得增加搜索的编号和数量
    }
    else
    {
        for (int k = 0; k < 26; k ++ )//将a~z每个数顺序遍历,这样可以保证得到的序列是按照字典序排列的
        {
            int na = -1; 
            int nb = -1; //na,nb用来记录下一个相同的字母是哪一个

            for (int x = i; x < la; x ++ )//遍历a串
                if (a[x] == 'a' + k)
                {
                    na = x;
                    break;
                }

            for (int x = j; x < lb; x ++ )//遍历b串
                if (b[x] == 'a' + k)
                {
                    nb = x;
                    break;
                }

            if (na != -1 && nb != -1 && f[na][nb] == f[i][j] + 1)//这里继续递归下去的条件:首先必须na,nb存在,其次,由于f[i][j]不一样,而f[na][nb]一样,所以在这里
            //的值要等于f[na][nb] == f[i][j] + 1,不能比这个数大,如果比它大的话,就说明中间漏掉了一些相同的数,这种方案就不一定属于最优的
            {
                dfs(na, nb, step);
            }
        }
    }
}
int main()
{
	cin >> a >> b;
	la = a.size(),lb = b.size();
	for(int i = 0;i < la;i ++)
		for(int j = 0;j < lb;j ++)
		{
			f[i][j] = max(f[i - 1][j],f[i][j - 1]);
			if(a[i] == b[j])
				f[i][j] = max(f[i - 1][j - 1] + 1,f[i][j]);
		}//先dp求最大子序列的长度

	len = f[la - 1][lb - 1];
	dfs(0,0,1);//深搜
	return 0;
}

动物园

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10,M = 1 << 5;

int f[N][M];//代表当围栏从i到i+4时,状态为j时的总的最多小朋友高兴数
int num[N][M];//代表当围栏从i到i+4时,状态为j时的小朋友高兴数,注意实际上j的顺序是第0位对应i,以此类推
int n,m;

void init()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i ++)
	{
		int e,f,l,fear = 0,like = 0,a,b;
		scanf("%d%d%d",&e,&f,&l);
		for(int j = 1;j <= f;j ++)
		{
			scanf("%d",&a);
			a = (a - e + n) % n;
			fear |= 1 << a;//1代表害怕的动物数。
		}
		for(int j = 1;j <= l;j ++)
		{
			scanf("%d",&b);
			b = (b - e + n) % n;
			like |= 1 << b;
		}
		for(int j = 0;j < 32;j ++)
			if((j & fear) || (~j & like)) ++ num[e][j];//这里看仔细一点
	}
}
int main()
{
	init();
	int ans = 0;
	for(int i = 0;i < 32;i ++)//枚举每一次开始的状态
	{
		memset(f[0],-0x3f,sizeof f[0]);//初始化成极小数
		f[0][i] = 0;//第零个围栏的状态为0(实际上对应的是第n个围栏的状态)

		for(int j = 1;j <= n;j ++)//枚举每一行
			for(int k = 0;k < 32;k ++)//枚举每一种状态
				f[j][k] = max(f[j - 1][(k & 15) << 1],f[j - 1][(k & 15) << 1 | 1] ) + num[j][k];
//此处说一下转移方程,每一种状况都可以由它上一个状态转移过来,转移时分为上一个状态第0位为0和1(不去掉和去掉),因为我们上面已经说过状态对应的储存顺序,所以就用((k & 15) << 1),然后第零位为0或1,加上当前这五个围栏
		if(ans < f[n][i]) ans = f[n][i];//结尾的状态与第0个状态相同,所以用f[n][i]
	}
	printf("%d",ans);
	return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值