初级dp刷题记录

普及~普及/提高

线性dp

P1002 [NOIP2002 普及组] 过河卒

P1002 [NOIP2002 普及组] 过河卒
题意:
一个二维方格,给定一个点为马的位置,以这个点为中心, 象棋中马能到的点都是无法到的点,这个马是固定的。卒在起点 ( 0 , 0 ) (0,0) (0,0),到终点 ( n , m ) (n,m) (n,m) 的路径有多少种。
思路:
起点 ( 0 , 0 ) (0,0) (0,0) 写转移方程不方便,需要写 i f if if 判边界,因此把坐标都加 1 1 1,显然答案是不变的,把不能到的点标记上,然后转移即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll f[maxn][maxn];
bool vis[maxn][maxn];
void work()
{
	int a, b, c, d;cin >> a >> b >> c >> d;
	++a,++b,++c,++d;
	vis[c][d] = 1;
	vis[c-1][d+2] = vis[c-1][d-2] = vis[c-2][d+1] = vis[c-2][d-1] = 1;
	vis[c+1][d-2] = vis[c+1][d+2] = vis[c+2][d+1] = vis[c+2][d-1] = 1;
	if(!vis[2][1]) f[2][1] = 1;
	if(!vis[1][2]) f[1][2] = 1;
	for(int i = 1; i <= a; ++i)
		for(int j = 1; j <= b; ++j) if(!vis[i][j])
		{
			f[i][j] += f[i-1][j] + f[i][j-1];
		}
	cout << f[a][b];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1057 [NOIP2008 普及组] 传球游戏

P1057 [NOIP2008 普及组] 传球游戏
题意:
n n n 个人围成一个圈,初始球在 1 1 1 号同学手上,每次可以选择将球传给左边或者右边的同学,在传球 m m m 次之后,计算球又回到 1 1 1 号同学手上的方法又多少种(两种方法不同:接到球的同学按接球顺序组成的序列是不同的
思路:
第一步:确定状态(动态变量)——原问题是什么,子问题是什么?
原问题:从 1 1 1 开始传球,第 m m m 步回到 1 1 1 号的情况数
子问题:从 1 1 1 开始传球第 i i i 步到达 j j j 号的情况数
d p [ i ] [ j ] dp[i] [j] dp[i][j] 表示第 i i i 次传球后在 j j j 位置的情况数
第二步:确定状态转移方程和边界
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j + 1 ] dp[i] [j] = dp[i - 1] [j - 1] + dp[i - 1] [j + 1] dp[i][j]=dp[i1][j1]+dp[i1][j+1]
d p [ 0 ] [ 1 ] = 1 dp[0] [1] = 1 dp[0][1]=1,初始条件一号开始的时候是1
是个环,需要对边界 1 1 1 m m m 进行讨论
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e2 + 9;
const int mod = 1e9 + 7;
ll n, m, k;
ll f[maxn][maxn];// f[i][j]表示前j位添加了i个乘号时的最优解
string s;
ll cal(int i, int j)//计算一段的值
{
	ll sum = 0;
	for(int k = i; k <= j; ++k)
		sum = sum * 10 + (s[k] - '0');
	return sum;
}
void work()
{
	cin >> n >> k >> s;
	s = "@" + s;
	for(int i = 1; i <= n; ++i) f[0][i] = cal(1, i);//边界,在没有乘号的情况下最优解总是前i位
	for(int i = 1; i <= k; ++i)
	{
		for(int j = i + 1; j <= n; ++j)
		{
			for(int h = j - 1; h >= i; --h)
				f[i][j] = max(f[i][j], f[i-1][h] * cal(h + 1, j));
		}
	}
	cout << f[k][n];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1018 [NOIP2000 提高组] 乘积最大(普及+/提高

P1018 [NOIP2000 提高组] 乘积最大
题意:
给定一个 n n n k k k,以及一个数字串 s s s n n n 表示数字串的长度, k k k 表示在这个数字串中插入 k k k 个乘号分成 k + 1 k+1 k+1 份,求最大乘积
思路:
(这个题拿60分,明白动态规划咋写就行了,高精就懒得搞了

code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e2 + 9;
const int mod = 1e9 + 7;
ll n, m, k;
ll f[maxn][maxn];// f[i][j]表示前j位添加了i个乘号时的最优解
string s;
ll cal(int i, int j)//计算一段的值
{
	ll sum = 0;
	for(int k = i; k <= j; ++k)
		sum = sum * 10 + (s[k] - '0');
	return sum;
}
// 1231  2
void work()
{
	cin >> n >> k >> s;
	s = "@" + s;
	for(int i = 1; i <= n; ++i) f[0][i] = cal(1, i);//边界,在没有乘号的情况下最优解总是前i位
	for(int i = 1; i <= k; ++i)
	{
		for(int j = 1; j <= n; ++j)
		{
			for(int h = j; h >= i; --h)
				f[i][j] = max(f[i][j], f[i-1][h-1] * cal(h, j));
		}
	}
	cout << f[k][n];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

other:
交换枚举顺序

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 200 + 9;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
ll n, m;
ll f[maxn][10];
string s;

ll cal(int i, int j){
	ll sum = 0;
	for(int k = i; k <= j; ++k)
		sum = sum * 10 + (s[k] - '0');
	return sum;				
}

void work()
{
	cin >> n >> m;
	cin >> s;s = "@" + s;
	for(int i = 1; i <= n; ++i) f[i][0] = cal(1, i);
	for(int i = 1; i <= n; ++i)
		for(int j = i; j >= 2; --j)
			for(int k = 1; k <= m; ++k)
				f[i][k] = max(f[i][k], f[j-1][k-1] * cal(j, i));		
	cout << f[n][m];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

U187635 刷墙(easy) 21级新生赛的题目

U187635 刷墙(easy)
(21级新生赛的题目
题意:
给定一个被分成 n n n 块的墙壁, m m m 个工人, a i a_i ai 表示墙块需要刷成的的颜色,刷墙块只能刷连续的子段,如果一个工人刷的一段连续的墙为 { 1 , 1 , 1 , 2 , 2 , 3 , 2 , 2 1,1,1,2,2,3,2,2 1,1,1,2,2,3,2,2},那么应该支付 3 2 + 2 2 + 1 2 + 2 2 = 18 3^2+2^2+1^2+2^2=18 32+22+12+22=18,也就是所有连续的相同颜色子段的长度的平方和。求刷完所有墙的最少花费
思路:
(这道题比一般的还是难一些的,和上边的乘积最大很像
考虑 f [ i ] [ j ] f[i][j] f[i][j] j j j 个工人刷 前 i i i 块墙的花费
那么 f [ i ] [ j ] = m i n ( f [ k ] [ j − 1 ] ( k ∈ [ j − 1 , i ) ) + w [ k + 1 ] [ i ] , f [ i ] [ j ] ) f[i][j]=min({f[k][j-1](k\in[j-1,i))} + w[k+1][i],f[i][j]) f[i][j]=min(f[k][j1](k[j1,i))+w[k+1][i],f[i][j])
显然 w w w 数组需要预处理出来,才能进行状态转移
w w w 数组的预处理也需要用动态规划的思想解决:
考虑 w [ i ] [ j ] w[i][j] w[i][j] 为粉刷 [ i , j ] [i,j] [i,j] 这段墙的花费,我们可以枚举左端点,考虑以 i i i 为起点的递推到 n n n,当前连续的长度为 c n t cnt cnt,如果下一个也相同,那么 根据 ( c n t + 1 ) 2 − c n t 2 = 2 ∗ c n t + 1 (cnt+1)^2-cnt^2=2*cnt+1 (cnt+1)2cnt2=2cnt+1 即可得到比 w [ i ] [ j ] w[i][j] w[i][j] 多出的花费, w [ i ] [ j ] = w [ i ] [ j − 1 ] + 2 ∗ c n t + 1 w[i][j]=w[i][j-1]+2*cnt+1 w[i][j]=w[i][j1]+2cnt+1,不相等则多出的花费就是 1 1 1
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn];
ll f[2009][29];
ll w[maxn][maxn];
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i) cin >> a[i], w[i][i] = 1;
	for(int i = 1; i <= n; ++i)
		for(int j = i + 1, cnt = 0; j <= n; ++j)
			{
				if(a[j] == a[j-1]) ++cnt;
				else cnt = 0;
				w[i][j] = w[i][j-1] + cnt * 2 + 1;
			}
	memset(f, 0x3f, sizeof(f));
	f[0][0] = 0;
	for(int i = 1; i <= n; ++i){
		for(int j = 1; j <= min(1ll * i, m); ++j){// 后两个枚举顺序可变
			for(int k = j - 1; k < i; ++k)
				f[i][j] = min(f[i][j], f[k][j-1] + w[k + 1][i]);
		}
	}
	cout << f[n][m];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

牛客题—过桥

牛客题—过桥
题意:
n n n 个浮块按照 1 − n 1-n 1n 标号,每块上边有一个数字 a i a_i ai,如果是正数,那么它可以移动到第 i + k ( k ∈ [ 1 , a [ i ] ] i+k(k\in[1,a[i]] i+k(k[1,a[i]] 块浮块上,负数表示它可以移动到 < = i + a [ i ] <=i+a[i] <=i+a[i] 的任意一块浮块,当 i + a [ i ] < 1 i+a[i]<1 i+a[i]<1 则传送到 1 1 1。每次花费一秒,求从 1 1 1 n n n 的最短时间
思路:
这题虽然暴力建图就能过,但是这是道dp题
首先不考虑 a i a_i ai 是负数的情况,那么它从 1 1 1 传到 i i i 的步数显然是逐渐递增的, f [ i ] f[i] f[i] 是一个非递减数组。当出现 a i < 0 a_i<0 ai<0 时,它只能往回跳, f [ j ] = m i n ( f [ j ] , f [ i ] + 1 ) ( j < i ) f[j]=min(f[j],f[i]+1)(j < i) f[j]=min(f[j],f[i]+1)(j<i),这显然是没必要的更新,他不会发生更新。
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn], f[maxn];
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i)
		cin >> a[i];
	memset(f, 0x3f, sizeof(f));
	f[1] = 0;
	for(int i = 1; i <= n; ++i)
	{
		if(a[i] > 0)
		{
			for(int j = i + 1; j <= i + a[i]; ++j)
				f[j] = min(f[j], f[i] + 1);
		}
	}
	if(f[n] != 1061109567)cout << f[n];
	else cout << -1;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

CF414B Mashmokh and ACM

CF414B Mashmokh and ACM
题意:
如果一个数组的后一个可以被前一个整除那么就是一个好数列
给定一个 n n n k k k,求数组中最大元素不超过 n n n,长度为 k k k好数列的个数。
1 < = n , k < = 2000 1<=n,k<=2000 1<=n,k<=2000
思路:
计数dp,需要预处理每个数的因子
考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示最大数 i i i,长度为 j j j 时的满足条件的数列数量
边界 f [ 1 ] [ 0 ] = 1 f[1][0]=1 f[1][0]=1 i s is is i m p o r t a n t important important
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll f[maxn][maxn];
vector <int> v[maxn];
void work()
{
	for(int i = 1; i <= 2000; ++i){
		for(int j = 1; j <= i; ++j){
			if(i % j == 0){
				v[i].push_back(j);
			}
		}
	}
	cin >> n >> m;
	f[1][0] = 1;// n最小为 1 
	for(int i = 2; i <= n; ++i)
		for(int j = 1; j <= m; ++j)
		{
			for(int k = 0; k < v[i].size(); ++k)// 由i的因子转移过来
				f[i][j] = (f[i][j] + f[v[i][k]][j-1]) % mod;
		}
	ll ans = 0;
	for(int i = 1; i <= n; ++i)
		ans = (ans + f[i][m]) % mod;
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1077 [NOIP2012 普及组] 摆花

P1077 [NOIP2012 普及组] 摆花
题意:
给定一个序列 n n n 个数, m m m 个花盆, a i a_i ai 表示第 i i i 种花最多放 a i a_i ai 朵,要求放花的顺序按照编号来,同一种花只能连着放,求一共有多少种不同的摆花方案。
思路:
计数 d p dp dp,考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 种花放了 j j j
那么转移方程 f [ i ] [ j ] + = f [ i − 1 ] [ j − k ] ( k ∈ [ 0 , m i n ( j , a [ i ] ) ] f[i][j]+=f[i-1][j-k](k\in[0,min(j,a[i])] f[i][j]+=f[i1][jk](k[0,min(j,a[i])]
这题要注意枚举 j j j k k k 都要从 0 0 0 开始
多思考边界枚举边界问题
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const int mod = 1e6 + 7;
ll n, m;
int a[maxn];
int f[maxn][maxn];
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i) cin >> a[i];
	f[0][0] = 1;
	for(int i = 1; i <= n; ++i)
		for(int j = 0; j <= m; ++j)
			for(int k = 0; k <= min(j, a[i]); ++k)
				f[i][j] = (f[i][j] + f[i-1][j-k]) % mod;
	cout << f[n][m];             
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

LIS—P1091 [NOIP2004 提高组] 合唱队形

LIS—P1091 [NOIP2004 提高组] 合唱队形
题意:
给定 n n n 个高度 a i a_i ai,从中选出 m m m 个,满足 t 1 < t 2 < . . . < t i > t i + 1 > . . . > t m t_1<t_2<...<t_i>t_{i+1}>...>t_m t1<t2<...<ti>ti+1>...>tm,最大的 m m m 是多少,输出 n − m n-m nm 即可
思路:
考虑从前往后和从后往前求得两个最长上升子序列,枚举最高点维护最大值即可。
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const ll mod = 1000000000;
ll n, m;
int a[maxn];
int fh[maxn], fl[maxn];
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i];
	for(int i = 1; i <= n; ++i){
		fh[i] = 1;
		for(int j = 1; j < i; ++j) if(a[j] < a[i]){
			fh[i] = max(fh[i], fh[j] + 1);
		}
	}
	for(int i = n; i >= 1; --i){
		fl[i] = 1;
		for(int j = n; j > i; --j) if(a[j] < a[i]){
			fl[i] = max(fl[i], fl[j] + 1); 
		}
	}
	int ans = 0;
	for(int i = 1; i <= n; ++i)
		ans = max(ans, fl[i] + fh[i] - 1);
	cout << n - ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P2513 [HAOI2009]逆序对数列(需优化

P2513 [HAOI2009]逆序对数列
题意:
给定 n , k n,k n,k,求 1 − n 1-n 1n 的全排列中逆序对为 m m m 的数量
思路:
考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示 1 − i 1-i 1i 的全排列中逆序对为 j j j 的数量
初始化 f [ 1 ] [ 0 ] = 1 f[1][0]=1 f[1][0]=1
考虑在 i − 1 i-1 i1 的排列中加入 i i i,增加的逆序对最少是 0 0 0,最多会增加 i − 1 i-1 i1
枚举增加的逆序对 k k k
f [ i ] [ j ] = ∑ k = 0 m i n ( i − 1 , j ) f [ i − 1 ] [ j − k ] f[i][j]=\sum_{k=0}^{min(i-1,j)}f[i-1][j-k] f[i][j]=k=0min(i1,j)f[i1][jk]
这么做会 T L E TLE TLE
考虑优化第三层循环,这是一个用于累加的 f o r for for,显然可以用前缀和优化
但是根据上述式子不好推导优化,因为 j − k j-k jk 的累加无法用前缀和描述,因此改变一下上边式子的形式
∵ k ∈ [ 0 , m i n ( i − 1 , j ) ] ∴ − k ∈ [ − m i n ( i − 1 , j ) , 0 ] ∴ j − k ∈ [ j − m i n ( i − 1 , j ) , j ] ⟹ j − k ∈ [ m a x ( 0 , j − i + 1 ) , j ] ⟹ f [ i ] [ j ] = ∑ k = m a x ( 0 , j − i + 1 ) j f [ i − 1 ] [ k ] \because k\in[0,min(i-1,j)]\\ \therefore -k\in[-min(i-1,j),0]\\ \therefore j-k\in[j-min(i-1,j),j] \\ \Longrightarrow j-k\in[max(0,j-i+1),j]\\ \Longrightarrow f[i][j]=\sum_{k=max(0,j-i+1)}^{j}f[i-1][k] k[0,min(i1,j)]k[min(i1,j),0]jk[jmin(i1,j),j]jk[max(0,ji+1),j]f[i][j]=k=max(0,ji+1)jf[i1][k]
做完这个变形就可以用一个 s u m sum sum 来记录前缀和
注意,当 j > i − 1 j>i-1 j>i1 时,需要减掉起点 j − ( i − 1 ) j-(i-1) j(i1) 之前的
T L E TLE TLEcode:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 10000;
ll n, m;
ll f[maxn][maxn];
void work()
{
	cin >> n >> m;
	f[1][0] = 1;
	for(int i = 2; i <= n; ++i)
		for(int j = 0; j <= m; ++j)
			for(int k = 0, t = min(i-1, j); k <= t; ++k)//枚举增加的逆序对 $k$
				{
					f[i][j] = (f[i][j] + f[i-1][j - k]) % mod;
				}
	cout << f[n][m];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

A C AC ACcode:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 10000;
ll n, m;
int f[maxn][maxn];
void work()
{
	cin >> n >> m;
	f[1][0] = 1;
	for(int i = 2; i <= n; ++i)
	{
		int sum = 0;
		for(int j = 0; j <= m; ++j)
		{
			sum = (sum + f[i-1][j]) % mod;
			if(j > i - 1)
				sum = ((sum - f[i-1][j - (i-1) - 1]) % mod + mod) % mod;			
			f[i][j] = sum;

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

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P2066 机器分配

P2066 机器分配
题意:
给定 n n n 个公司, m m m 台机器,矩阵 A i , j A_{i,j} Ai,j 表示第 i i i 个公司分配到 j j j 台机器所带来的利润,求分配完 m m m 台机器的最大利润
思路:
这道题的状态是公司和机器数量,考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 个公司分配 j j j 台机器的最大利润,那么 f [ i ] [ j ] f[i][j] f[i][j] 可以由 f [ i − 1 ] [ j − k ] + m p [ i ] [ k ] ( k ∈ [ 0 , j ] ) f[i-1][j-k]+mp[i][k](k\in[0,j]) f[i1][jk]+mp[i][k](k[0,j]) 转移过来。
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll mp[maxn][maxn];
ll f[maxn][maxn];// 前i个公司总共分配j台机器的最大利润
void print(int i, int j)
{
	if(!i) return;
	for(int k = j; k >= 0; --k) if(f[i][j] == f[i-1][j-k] + mp[i][k])
	{// 找字典序最小,那就尽量让后边的数大,这样前边就尽量分配的小
		print(i-1, j-k);
		cout << i << " " << k << endl;
		return;
	}
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= m; ++j)
			cin >> mp[i][j];

	for(int i = 1; i <= n; ++i)
		for(int j = 0; j <= m; ++j)
		{
			for(int k = 0; k <= j; ++k)
				f[i][j] = max(f[i][j], f[i-1][j-k] + mp[i][k]);
		}
	cout << f[n][m] << endl;
	print(n, m);
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1107 [BJWC2008]雷涛的小猫(需优化,普及+/提高

P1107 [BJWC2008]雷涛的小猫
题意:
给定 n n n 棵树,每个树的高度都是 h h h,输入第 i i i 行第一个数为这棵树上的果子个数 n u m num num,然后输入 n u m num num 个果子所在的高度,一棵树上果子高度可能重复。小猫可以任选一棵高度 h h h 的树顶,然后在一棵树上下来时是连续的,但是如果从一棵树跳到另外一棵树上就会下降 d d d 高度,求小猫最多能吃多少果子。
思路:
考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示小猫在第 j j j 棵树上,高度为 i i i 时最多能吃的果子数量
a [ i ] [ j ] a[i][j] a[i][j] 表示第 j j j 棵树上高度为 i i i 的果子数量
状态转移方程很好想,要么顺着这棵树下来,要么从某个树上跳过来
f [ i ] [ j ] = m a x ( f [ i + 1 ] [ j ] , f [ i + d ] [ k ] )   ( k   ! = j ) + a [ i ] [ j ] f[i][j] = max(f[i+1][j],f[i+d][k]) \ (k \ !=j ) + a[i][j] f[i][j]=max(f[i+1][j],f[i+d][k]) (k !=j)+a[i][j]
看数据这显然会 T T T
T L E TLE TLE code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 10000;
ll n, m, d;
ll f[maxn][maxn];
int a[maxn][maxn];
void work()
{
	cin >> n >> m >> d;
	for(int i = 1; i <= n; ++i)
	{
		int num;cin >> num;
		for(int j = 1; j <= num; ++j)
		{
			int x;cin >> x;
			a[x][i]++;
		}
	}
	ll ans = 0;
	for(int i = m; i >= 1; --i)
	{
		for(int j = 1; j <= n; ++j)
		{
			f[i][j] = f[i+1][j] + a[i][j];// 不跳,顺着第j颗向下移动
			if(i + d <= m)// 跳跃转移 
			for(int k = 1; k <= n; ++k) if(j != k)
				f[i][j] = max(f[i][j], f[i+d][k] + a[i][j]);
		}
	}
	for(int i = 1; i <= n; ++i)
		ans = max(ans, f[1][i]);
	cout << ans;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

仔细观察状态转移方程,第三层 f o r for for 无非就是去获得 i + d i+d i+d 高度上最多的一个状态,我们只需要转移过来的时候维护一个高度为 j ( j + d < = h ) j(j+d<=h) j(j+d<=h) 时的最多果子的一个状态即可。
A C AC AC code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 10000;
ll n, m, d;
ll f[maxn][maxn];
int a[maxn][maxn];
ll h[maxn];
void work()
{
	cin >> n >> m >> d;
	for(int i = 1; i <= n; ++i)
	{
		int num;cin >> num;
		for(int j = 1; j <= num; ++j)
		{
			int x;cin >> x;
			a[x][i]++;
		}
	}
	ll ans = 0;
	for(int i = m; i >= 1; --i)
	{
		for(int j = 1; j <= n; ++j)
		{
			f[i][j] = f[i+1][j] + a[i][j];// 不跳,顺着第j颗向下移动
			if(i + d <= m)// 跳跃转移 
				f[i][j] = max(f[i][j], h[i+d] + a[i][j]);
			h[i] = max(h[i], f[i][j]);
		}
	}
	for(int i = 1; i <= n; ++i)
		ans = max(ans, f[1][i]);
	cout << ans;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P2782 友好城市

P2782 友好城市
题意:
一条横着的河两岸边有各有 n n n 个城市, n n n 行每行给定上边城市和下边城市的坐标表示两个城市一条边,由于可能出现边的交叉,但我们不想交叉,因此需要求出在边不交叉的情况下最多能画出多少条边。
思路:
按一边排序,然后对另一边求最长上升子序列
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 1e6 + 9;
const int mod = 1e9 + 7;
ll n, m;
struct node{
	int x, y;
	bool operator<(const node &B)const{
		return x < B.x;
	}
}a[maxn];
int f[maxn];
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i].x >> a[i].y;
	sort(a + 1, a + 1 + n);
	f[1] = a[1].y;
	int cnt = 1;
	for(int i = 2; i <= n; ++i)
	{
		if(a[i].y > f[cnt]){
			f[++cnt] = a[i].y;
		}
		else
		{
			int pos = lower_bound(f + 1, f + 1 + cnt, a[i].y) - f;
			f[pos] = a[i].y;
		}
	}
	cout << cnt;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1439 【模板】最长公共子序列

P1439 【模板】最长公共子序列
题意:
给定两个 n n n 的排列,求最长公共子序列( n < = 1 0 5 ) n<=10^5) n<=105)
思路:
这个题可以把排列 a a a 映射成 1 , 2 , 3... n 1,2,3...n 1,2,3...n 的排列,按照这个映射修改 b b b
这个过程其实就是一个文本替换,显然 L C S LCS LCS 是不变的
但是我们得到了一个递增的排列 a a a,思考可以发现我们只需要找排列 b b b 中的 L I S LIS LIS 即可,因为这个最长上升子序列一定可以在 a a a 中找到
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn], b[maxn];
int c[maxn];
int f[maxn];
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i], c[a[i]] = i;
	for(int i = 1; i <= n; ++i) cin >> b[i];
	for(int i = 1; i <= n; ++i) b[i] = c[b[i]];
	//for(int i = 1; i <= n; ++i) cout << b[i] << " ";cout << endl;
	f[1] = b[1];
	int c = 1;
	for(int i = 2; i <= n; ++i)
	{
		if(b[i] > f[c]){
			f[++c] = b[i];
		}
		else
		{
			int pos = lower_bound(f + 1, f + 1 + c, b[i]) - f;
			f[pos] = b[i];
		}
	}
	cout << c;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

矩阵dp

P1387 最大正方形

P1387 最大正方形
题意:
给定一个 01 01 01 矩阵,求边长最大的全 1 1 1 正方形。
思路:
考虑 f [ i ] [ j ] f[i][j] f[i][j] 为以 ( i , j ) (i,j) (i,j) 为右下角时的最大正方形边长
这个状态转移方程比较巧妙:
f [ i ] [ j ] = m i n ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] , f [ i − 1 ] [ j − 1 ] ) + 1   ( a [ i ] [ j ] = 1 ) f[i][j]=min(f[i-1][j],f[i][j-1],f[i-1][j-1])+1 \ (a[i][j]=1) f[i][j]=min(f[i1][j],f[i][j1],f[i1][j1])+1 (a[i][j]=1)
详解博客
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn][maxn], f[maxn][maxn];
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= m; ++j)
			cin >> a[i][j];
	int ans = 0;
	for(int i = 1; i <= n; ++i)
	{
		for(int j = 1; j <= m; ++j){
			if(a[i][j])
				f[i][j] = min({f[i-1][j], f[i][j-1], f[i-1][j-1]}) + 1;
			ans = max(ans, f[i][j]);
		}
	}
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1681 最大正方形II

P1681 最大正方形II
题意:
给定一个 n × m n\times m n×m 01 01 01 矩阵,求 01 01 01 交错的最大正方形边长
思路:
f [ i ] [ j ] [ 0 / 1 ] f[i][j][0/1] f[i][j][0/1] 表示以 ( i , j ) (i,j) (i,j) 为右下角时的最大正方形边长
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn][maxn], f[maxn][maxn][2];
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= m; ++j)
			cin >> a[i][j];
	int ans = 0;
	for(int i = 1; i <= n; ++i)
	{
		for(int j = 1; j <= m; ++j){
			if(a[i][j] == 0)
				f[i][j][0] = min({f[i-1][j][1], f[i][j-1][1], f[i-1][j-1][0]}) + 1;
			else 
				f[i][j][1] = min({f[i-1][j][0], f[i][j-1][0], f[i-1][j-1][1]}) + 1;
			ans = max(ans, f[i][j][0]);
			ans = max(ans, f[i][j][1]);
		}
	}
	cout << ans;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1434 [SHOI2002]滑雪

P1434 [SHOI2002]滑雪
题意:
给定一个矩阵,每个位置一个高度,一个人可以从某个点沿着严格递减的高度移动,求最长移动的距离
思路:
经典题目,记忆化搜索即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int mp[109][109];
int f[109][109];
bool check(int x, int y){
	if(x < 1 || y < 1 || x > n || y > m) return false;
	return true;
}
int dfs(int i, int j){
	if(f[i][j]) return f[i][j];
	f[i][j] = 1;
	for(int k = 0; k < 4; ++k){
		int x = i + dx[k], y = j + dy[k];
		if(check(x, y) && mp[x][y] < mp[i][j]){
			f[i][j] = max(f[i][j], dfs(x, y) + 1);
		}
	}
	return f[i][j];
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i){
		for(int j = 1; j <= m; ++j){
			cin >> mp[i][j];
		}
	}
	int ans = 0;
	for(int i = 1; i <= n; ++i){
		for(int j = 1; j <= m; ++j){
			ans = max(ans, dfs(i, j));
		}
	}
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1854 花店橱窗布置

P1854 花店橱窗布置
题意:
n n n 个花, m ( m > = n ) m(m>=n) m(m>=n) 个花瓶,花和花瓶从左到右被标上 i i i,如果 i < j i<j i<j,那么花束 i i i 必须放在花束 j j j 的左边,放完后的花束从左到右必须满足原来的顺序,每个花瓶只能放一朵花,所有花都要放完。接下来一个矩阵 A i , j A_{i,j} Ai,j 表示第 i i i 朵花放在第 j j j 个花瓶中的美观度,求最大美观度。
思路:
把题目抽象出来,就是给定一个矩阵,从 ( 1 , 1 ) (1,1) (1,1) 开始往右下移动,每次先向右移动 k k k 格,然后向下移动一格,求移动到最后一行时的最大值(最后一行也要选定一个 k k k 向右移动,不用向下了
考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 朵花插到前 j j j 个花瓶中的最大美观度,那么 f [ i ] [ j ] f[i][j] f[i][j] 可以由 m a x ( f [ i − 1 ] [ i − 1 ] , f [ i − 1 ] [ i ] . . . f [ i − 1 ] [ j − 1 ] ) + a [ i ] [ j ] max(f[i-1][i-1],f[i-1][i]...f[i-1][j-1]) + a[i][j] max(f[i1][i1],f[i1][i]...f[i1][j1])+a[i][j] 转移过来,那么答案就在 f [ n ] [ n ] f[n][n] f[n][n] f [ n ] [ m ] f[n][m] f[n][m] 之间
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll mp[maxn][maxn];
ll f[maxn][maxn];
void print(int i, int j)
{
	if(!i) return;
	for(int k = i - 1; k <= j - 1; ++k) if(f[i][j] == f[i - 1][k] + mp[i][j])
	{
		print(i - 1, k);
		cout << j << " ";
		break;
	}
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= m; ++j)
			cin >> mp[i][j];
			
	memset(f, -0x3f, sizeof(f));		
	for(int i = 0; i <= m; ++i)	f[0][i] = 0;
	for(int i = 0; i <= n; ++i)	f[i][0] = 0;
	
	for(int i = 1; i <= n; ++i)
		for(int j = i; j <= m - (n - i); ++j)
		{// 所有花都要放完对 j和k的范围有约束
			for(int k = i - 1; k <= j - 1; ++k)
				f[i][j] = max(f[i][j], f[i-1][k] + mp[i][j]);
		}
	ll ans = -2e9, pos;
	for(int i = n; i <= m; ++i) if(ans < f[n][i])
		ans = f[n][i], pos = i;
	cout << ans << endl;
	print(n, pos);
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1006 [NOIP2008 提高组传纸条(优化,普及+/提高

P1006 [NOIP2008 提高组] 传纸条
题意:
给定一个 m × n m\times n m×n 的矩阵,每个位置有一个好感度值,求两个路径上好感度加和的最大值,从 ( 1 , 1 ) (1,1) (1,1) ( m , n ) (m,n) (m,n) 和从 ( m , n ) (m,n) (m,n) ( 1 , 1 ) (1,1) (1,1) 两条路径,要求路径不能有交点。
思路:
基础的四位数组解法:
这个两个路径可以等价为从 1 , 1 1,1 1,1 开始 到 m , n m,n m,n 结束的两个不相交路径
于是可以想象两个人 A , B A,B A,B 1 , 1 1,1 1,1 出发
d p [ i ] [ j ] [ k ] [ l ] dp[i][j][k][l] dp[i][j][k][l] 定为 A A A 走到 i , j i,j i,j B B B 走到 k , l k,l k,l 的最大好感度和
这道题目的一个关键就是要保证两人同步移动,然后使得二人横纵坐标之和相同(即在同一对角线
然后 O ( n 4 ) O(n^4) O(n4) 进行转移就好了
code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 55;

int a[maxn][maxn];
int dp[maxn][maxn][maxn][maxn];
int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
            cin >> a[i][j];
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
            for(int k = 1; k < i; ++k)
            {
                int l = i + j - k;
                if(l > m) continue;
                dp[i][j][k][l] = max({dp[i-1][j][k-1][l], dp[i-1][j][k][l-1], dp[i][j-1][k-1][l], dp[i][j-1][k][l-1]})+ a[i][j] + a[k][l];
			}
    cout << dp[n][m-1][n-1][m] << '\n';
    return 0;
}

三维解法:
题解
code:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=60;
int a[maxn][maxn];
int F[2*maxn][maxn][maxn];// F[k][i][j] 代表步数为 k 时, 一个人到第 i 列,另一个人到第 j 列时的最大价值  
int main()
{
  int m, n;
  scanf("%d %d",&m, &n);
  for(int i = 1;i <= m; i++)
    for(int j = 1;j <= n; j++)
      scanf("%d", &a[i][j]);
  //F[sum][i][j]=max{F[sum-1][i][j]...
  for(int k = 3; k < m + n; k++)
    for(int i = 1;i < n; i++)
      for(int j = i + 1; j <= n; j++)
      {
      	if(k - i < 1 || k - j < 1) continue;
        int s = max({F[k][i][j], F[k-1][i][j], F[k-1][i-1][j], F[k-1][i][j-1], F[k-1][i-1][j-1]});
        F[k][i][j] = s + a[k-i][i] + a[k-j][j];//该点的值为最大的前一个值与当前F[k][i][j]表示两点的值的和。 
      }
  printf("%d",F[m+n-1][n-1][n]);//因为i永远小于j,所以右下角的点不会求到,
  //但是到右下角只有一种情况,就是在右下角的上面和右下角的左边,直接输出就好了。 
  return 0;
 } 

背包dp

P1064-NOIP2006 提高组-金明的预算方案

P1064-NOIP2006 提高组-金明的预算方案
思路:
感觉写起来好麻烦的一道题,主要就是处理好物品
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll f[maxn];
struct node{
	int v[5], w[5], cnt;
}a[maxn];

void work()
{
	cin >> n >> m;
	for(int i = 1; i <= m; ++i)
	{
		int v, p, q;cin >> v >> p >> q;
		if(!q){
			a[i].v[1] = v;
			a[i].w[1] = v * p;
			a[i].cnt++;
			
		}
		else
		{
			if(a[q].cnt == 1){
				a[q].cnt++;
				a[q].v[2] = a[q].v[1] + v;
				a[q].w[2] = a[q].w[1] + v * p;
			}
			else
			{
				a[q].cnt = 4;
				a[q].v[3] = a[q].v[1] + v;
				a[q].w[3] = a[q].w[1] + v * p;
				a[q].v[4] = a[q].v[2] + v;
				a[q].w[4] = a[q].w[2] + v * p;
			}
		}
	}
	for(int i = 1; i <= m; ++i)
	{
		for(int j = n; j >= 0; --j)
		{
			for(int k = 1; k <= a[i].cnt; ++k)
			{
				if(j >= a[i].v[k])
					f[j] = max(f[j], f[j - a[i].v[k]] + a[i].w[k]);
			}
		}
	}
	cout << f[n];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1586 四方定理

P1586 四方定理
题意:
给定一个数 n n n,把 n n n 分解成不超过 4 4 4 个正整数的平方和,方案有多少种
思路:
n n n 看成背包容量,那么每个数的平方和就是物品的体积和价值,同种物品可以重复选,就是完全背包求方案数,类似于硬币凑钱方案数,但是这道题目限制了使用物品的数量,不超过 4 4 4 个,因此需要加一维维护。
其次完全背包的循环顺序是毫无疑问的,问题在于枚举物品个数的 f o r for for 位置,这个 f o r for for 可以放在枚举背包容量之前,也可以放在枚举背包容量之前。
就是我们要枚举某个数的使用个数,而不是去枚举要使用几个数,而枚举某个数的使用个数显然要放到枚举使用哪个数里边
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 1e6 + 7;
ll n, m;
ll f[5][maxn];// f[i][j]表示用i个数的平方和表示出j 
void init(int n)
{
	f[0][0] = 1;
	// 错误做法 
	/*for(int i = 1; i <= 4; ++i)
		for(int k = 1; k * k <= n; ++k)
			for(int j = k * k; j <= n; ++j)
				f[i][j] += f[i-1][j - k * k];*/
				
	// 正确做法
	for(int k = 1; k * k <= n; ++k)
		for(int i = 1; i <= 4; ++i)// // 枚举物品 k*k 的使用个数 
			for(int j = k * k; j <= n; ++j)
				f[i][j] += f[i-1][j - k * k];
	// or 
	/*for(int k = 1; k * k <= n; ++k)
		for(int j = k * k; j <= n; ++j)
			for(int i = 1; i <= 4; ++i)// 枚举物品 k*k 的使用个数 
				f[i][j] += f[i - 1][j - k * k];*/
}
void work()
{
	cin >> n;
	ll ans = 0;
	for(int i = 1; i <= 4; ++i) ans += f[i][n];
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	init(32768);
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P5020 [NOIP2018 提高组] 货币系统

P5020 [NOIP2018 提高组] 货币系统
题意:
给定 n n n 种面值的货币, a i a_i ai 表示货币面值,定义货币系统为 ( n , a ) (n,a) (n,a),两个货币系统 ( n , a ) , ( m , b ) (n,a),(m,b) (n,a),(m,b) 是等价,如果满足两个货币系统能表示出的数完全相同,即能表示出来的两个都能表示出来,不能表示的两个都不能表示,求一个与所给货币系统等价的货币系统,满足 m m m 最小,输出最小 m m m
思路:
可以猜一手结论,货币系统 b b b 的所有面值都在 a a a 中出现过,我们只需要去找,用 a a a 中的某些面值表示另一些 a a a 中面值,然后这些能被表示出的面值就没必要存在了,这样把最多能删除的面值删去,剩下的就是 m m m
把数组 a a a 从小到大排序,然后背包确定能被表示出的面值
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn];
bool f[maxn];
void work()
{
	memset(f, 0, sizeof(f));
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i];
	sort(a + 1, a + 1 + n);
	int ans = n;
	f[0] = 1;
	for(int i = 1; i <= n; ++i)
	{
		if(f[a[i]]) --ans;
		else
		{
			for(int j = a[i]; j <= a[n]; ++j)// 完全背包
				f[j] = f[j] | f[j - a[i]];
		}
	}
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

CF543A Writing Code

CF543A Writing Code
题意:
n n n 个程序员写 m m m 行代码,每个人可以写任意行代码,每个程序员一行代码会出现 a i a_i ai b u g s bugs bugs,要求 b u g s bugs bugs 数小于 k k k 的安排方案数。
思路:
n 3 n^3 n3 转移
类似于完全背包的问题
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 5e2 + 9;
int mod;
ll n, m, k;
int a[maxn];
int f[2][maxn][maxn];// f[i][j] 前 i个  写 j 行  d 个 bugs 
void work()
{
	cin >> n >> m >> k >> mod;
	for(int i = 1; i <= n; ++i) cin >> a[i];
	f[0][0][0] = 1;
	int op = 1;
	for(int i = 1; i <= n; ++i, op ^= 1)
		for(int j = 0; j <= m; ++j)
			for(int d = 0; d <= k; ++d)
			{
				f[op][j][d] = f[op ^ 1][j][d];
				if(j > 0 && d >= a[i])
					f[op][j][d] = (f[op][j][d] + f[op][j-1][d - a[i]]) % mod;
			}
	
	int ans = 0;
	for(int i = 0; i <= k; ++i)
		ans = (ans + f[op ^ 1][m][i]) % mod;	
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

区间dp

P1880 [NOI1995] 石子合并

P1880 [NOI1995] 石子合并
题意:
n n n 个石子堆围成圆,每次只能选择相邻的两个石子堆合并,并将合并后的数计为这次的得分,求最后只剩下一个石子堆时的最大得分
思路:
经典区间dp
主要最后要枚举一下区间,求答案
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn], sum[maxn];
ll f[200][200];
ll ff[200][200];
void work()
{
	memset(ff, 0x3f, sizeof(ff));
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i], a[n + i] = a[i];
	for(int i = 1; i <= n * 2; ++i)
		sum[i] = sum[i-1] + a[i], ff[i][i] = 0;
	for(int len = 2; len <= n; ++len)
		for(int l = 1; l + len - 1 <= 2 * n; ++l)
		{
			int r = l + len - 1;
			for(int k = l; k < r; ++k)
				f[l][r] = max(f[l][r], f[l][k] + f[k + 1][r] + sum[r] - sum[l-1]),
				ff[l][r] = min(ff[l][r], ff[l][k] + ff[k + 1][r] + sum[r] - sum[l-1]);
		}
	ll Max = 0, Min = 2e9;
	for(int i = 1; i <= n; ++i)
		Max = max(Max, f[i][i+n-1]),
		Min = min(Min, ff[i][i+n-1]);
	cout << Min << endl << Max;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

混入一道牛客题—跳跳跳

混入一道牛客题—跳跳跳
题意:
n n n 个格子呈环形分布,顺时针方向标号 1 − n 1-n 1n,其中 1 1 1 n n n 相邻,每个格子上有一个正整数 a i a_i ai,第 i i i 次跳跃,可以选择左边或者右边最近且尚未被跳跃过的位置进行一次跳跃,并获得 i × a [ p ] i\times a[p] i×a[p] 的得分,其中 p p p 为第 i i i 次跳跃的位置
思路:
一开始考虑的是 f [ i ] [ j ] f[i][j] f[i][j] 表示以 i i i 为起点跳 j j j 步获得最大的价值,然后发现不能转移,又开了两个两维的 f [ i ] [ j ] [ 2 ] [ 2 ] f[i][j][2][2] f[i][j][2][2] 来表示往左右跳的次数和获得的价值,然后感觉好麻烦。
这题正解是裸的区间dp,先变环成链,然后考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示左端点为 i i i,右端点为 j j j 时获得的最大价值。
仔细想想这个过程就和石子合并几乎一样
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 2e3 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll a[maxn << 1];
ll f[maxn << 1][maxn << 1];
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i], a[n + i] = a[i];
	for(int len = 1; len <= n; ++len)
		for(int l = 1; l + len - 1 <= 2 * n; ++l)
		{
			int r = l + len - 1;
			f[l][r] = max(f[l + 1][r] + len * a[l], f[l][r - 1] + len * a[r]);
		}
	ll ans = 0;
	for(int i = 1; i <= n + 1; ++i)
		ans = max(ans, f[i][i + n - 1]);
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P2426 删数

P2426 删数
题意:
给定 n n n 个不同的正整数,每次可以在序列最左边或最右边选一段序列删除,删除获得的价值为 a b s ( a i − a j ) ∗ ( j − i + 1 ) abs(a_i-a_j)*(j-i+1) abs(aiaj)(ji+1),其中 i , j i,j i,j 分别为序列的起点和终点,如果只删除一个数,价值即为删除的这个数 a i a_i ai,求删光序列后获得的最大价值。
思路:
考虑 f [ i ] [ j ] f[i][j] f[i][j] 表示把 [ i , j ] [i,j] [i,j] 子段删除时获得的最大价值。
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn], f[maxn][maxn];
int g(int i, int j){
	if(i == j) return a[i];
	return (j - i + 1) * abs(a[i] - a[j]);
}
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i];
	for(int len = 1; len <= n; ++len)
		for(int i = 1; i + len - 1 <= n; ++i)
		{
			int j = i + len - 1;
			f[i][j] = g(i, j);
			for(int k = i; k <= j; ++k)
				f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j]);
		}
	cout << f[1][n];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

优化成一维的解法:
题意唯一有干扰的是从后面删数的这个选择,但是稍微一想就知道每次从前面删数一直删到最后,最优解是不变的,最后留下的也是我们要从后面删去的区间。
考虑 f [ i ] f[i] f[i] 表示删除前 i i i 个数时产生的最大价值
状态转移方程 f [ i ] = m a x ( f [ i ] , f [ j ] + g ( j + 1 , i ) ( 1 < = j < i ) f[i]=max(f[i],f[j]+g(j+1,i)(1<=j<i) f[i]=max(f[i],f[j]+g(j+1,i)(1<=j<i)
其中, g g g 是一个估价函数(默认左端点小于等于右端点),它代表删除一个区间的价值。首先,前 i i i 个数可以一次删完,也可以分多次删。如果一次删,那么直接就是 v a l ( 1 , i ) val(1,i) val(1,i)。如果要多次删,我们就要找到对于第 i i i 个数,它和哪些数一起删除是最优的。这时枚举 j j j,用来找第 i i i 个数和哪些数一起删除。 d p [ j ] dp[j] dp[j] 代表和 i i i 无关的部分删除最大价值(这里体现了 d p dp dp 的转移性),而 v a l ( j + 1 , i ) val(j+1,i) val(j+1,i) 则是和 i i i 一起删除的价值。
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn], f[maxn];
int g(int i, int j){
	if(i == j) return a[i];
	return (j - i + 1) * abs(a[i] - a[j]);
}
void work()
{
	cin >> n;
	for(int i = 1; i <= n; ++i)
	{
		cin >> a[i];
		f[i] = g(1, i);
		for(int j = 1; j < i; ++j)
			f[i] = max(f[i], f[j] + g(j + 1, i));
	}
	cout << f[n];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P4290 [HAOI2008]玩具取名(普及+/提高

P4290 [HAOI2008]玩具取名
题意:
某人有一套玩具,并想法给玩具命名。首先他选择 W I N G WING WING 四个字母中的任意一个字母作为玩具的基本名字。然后他会根据自己的喜好,将名字中任意一个字母用 W I N G WING WING 中任意两个字母代替,使得自己的名字能够扩充得很长。给定一个字符串 s s s,输出最初选定的字符可能是哪些,按照 W I N G WING WING 的顺序输出,不存在则输出 “The name is wrong!”。
一行四个整数代表每个字母可由两个字母替换的方式的个数,然后 w w w 行,每行两个字母,代表 W W W 可由哪两个字母替换…
思路:
用数字代替字母, W I N G WING WING 依次映射成 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4
考虑 f [ l ] [ r ] [ k ] f[l][r][k] f[l][r][k] 表示区间 [ l , r ] [l,r] [l,r] 能由 k ∈ ( 1 , 2 , 3 , 4 ) k \in(1,2,3,4) k(1,2,3,4) 变形过来
枚举 k k k 和 它所能替换的组合,转移即可
注意需要初始化区间长度为 1 1 1 f f f 数组,区间 d p dp dp 的题目最好是先初始化长度为 1 1 1 的再转移其他的
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e2 + 9;
const int mod = 1e9 + 7;
ll n, m;
int num[5];
map <char,int> ma;
int a[5][20][3];
bool f[maxn][maxn][5];
char s[maxn];
char ans[10] = {"0WING"};
void work()
{
	for(int i = 1; i <= 4; ++i) ma[ans[i]] = i;
	for(int i = 1; i <= 4; ++i) cin >> num[i];
	for(int i = 1; i <= 4; ++i)
		for(int j = 1; j <= num[i]; ++j)
		{
			char x, y;cin >> x >> y;
			a[i][j][1] = ma[x];
			a[i][j][2] = ma[y];
		}
	cin >> (s + 1);
	n = strlen(s + 1);
	for(int i = 1; i <= n; ++i) f[i][i][ma[s[i]]] = 1;	
	
	for(int len = 2; len <= n; ++len)//枚举区间长度
		for(int l = 1, r = l + len - 1; l + len - 1 <= n; ++l, ++r)//枚举区间的左端点和区间的右端点 
			for(int k = l; k < r; ++k)//总的区间拼出来的数是由哪两个分区间组合出来的,也就是两个区间的交界处 
				for(int i = 1; i <= 4; ++i)//枚举WING 
					for(int j = 1; j <= num[i]; ++j)//WING能够被组合出来的方案 
					if(f[l][k][a[i][j][1]] && f[k+1][r][a[i][j][2]])
						f[l][r][i] = 1;

	vector <char> v;
	for(int i = 1; i <= 4; ++i) if(f[1][n][i])
		v.push_back(ans[i]);
	if(v.empty()) cout << "The name is wrong!";
	else 
		for(int i = 0; i < v.size(); ++i)
			cout << v[i];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

树形dp

P1352 没有上司的舞会

P1352 没有上司的舞会
题意:
给定一棵树 n n n 个节点,每个节点有一个权值 a i a_i ai,如果一个选定节点 i i i ,那么一定没有选择它的父节点,且不能选择它的子节点,求选出一些节点使得最大权值和。
思路:
树形dp即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e4 + 9;
const int mod = 1e9 + 7;
ll n, m;
int a[maxn];
bool vis[maxn];
ll f[maxn][2];
vector <int> e[maxn];
void dfs(int x){
	f[x][0] = 0;
	f[x][1] = a[x];
	for(int i = 0; i < e[x].size(); ++i){
		int d = e[x][i];
		if(vis[d]) continue;
		vis[d] = 1;
		dfs(d);
		f[x][0] += max(f[d][0], f[d][1]);
		f[x][1] += f[d][0];
	}
}
void work()
{
	cin >> n;	
	for(int i = 1; i <= n; ++i)cin >> a[i];
	for(int i = 1; i < n; ++i){
		int x, y;cin >> x >> y;
		e[x].push_back(y);
		e[y].push_back(x);
	}
	vis[1] = 1;
	dfs(1);
	cout << max(f[1][0], f[1][1]);
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

P1040 [NOIP2003 提高组] 加分二叉树

P1040 [NOIP2003 提高组] 加分二叉树
题意:
设一个 n n n 个节点的二叉树 tree \text{tree} tree 的中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,…,n) (1,2,3,,n),其中数字 1 , 2 , 3 , … , n 1,2,3,…,n 1,2,3,,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i i i 个节点的分数为 a i a_i ai tree \text{tree} tree 及它的每个子树都有一个加分,任一棵子树 subtree \text{subtree} subtree(也包含 tree \text{tree} tree 本身)的加分计算方法如下:
s u b t r e e subtree subtree 的左子树的加分 × s u b t r e e \times subtree ×subtree 的右子树的加分 + s u b t r e e + subtree +subtree 的根的分数。
若某个子树为空,规定其加分为 1 1 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,…,n) (1,2,3,,n) 且加分最高的二叉树 t r e e tree tree。要求输出
t r e e tree tree 的最高加分。
t r e e tree tree 的前序遍历。
思路:
code:

记忆化搜索

P3183 [HAOI2016]食物链

P3183 [HAOI2016]食物链
题意:
给定一张有向图, n n n 个点, m m m 条边,求有多少条食物链
思路:
建图记忆化搜索即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
vector <int> v[maxn];
ll f[maxn];
bool in[maxn];
int dfs(int x, int k){
	if(v[x].empty())
	{
		if(k == 1) return 0;
		else return 1;
	}
	if(f[x]) return f[x];
	ll ans = 0;
	for(int i = 0; i < v[x].size(); ++i)
	{
		ans += dfs(v[x][i], k + 1);
	}
	f[x] = ans;
	return ans;
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= m; ++i){
		int x, y;cin >> x >> y;
		v[x].push_back(y);
		in[y] = 1;
	}
	ll ans = 0;
	for(int i = 1; i <= n; ++i) if(!in[i])// 起点,生产者
		ans += dfs(i, 1);
		
	cout << ans;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

期望dp

P4316 绿豆蛙的归宿

P4316 绿豆蛙的归宿
题意:
n n n 个点 m m m 条边的无向图,起点未 1 1 1,终点为 n n n,每条边都有一个长度。若一个节点有 k k k 个节点,那么走每一个边的概率都为 1 k \frac{1}{k} k1。求 1 1 1 n n n 的总路长期望是多少。
思路:
参考大佬博客
期望dp经典模型
一般将问题直接作为 d p dp dp 的状态是最好的。
例如本题设状态 f [ x ] f[x] f[x] 表示点 x x x 到终点 n n n 的期望路径总长
有时期望 d p dp dp 需以最终状态为初始状态转移,即逆推。
一般来说,初始状态确定时可用顺推,终止状态确定时可用逆推
本题中显然 f [ n ] = 0 f[n]=0 f[n]=0,适合逆推的套路
假设一条边 x ⟶ y x\longrightarrow y xy
那么有 f [ x ] = 1 d e g [ x ] ∗ ∑ i = 1 d e g [ x ] ( f [ y ] + w [ x ⟶ y ] ) f[x]=\frac{1}{deg[x]}*\sum_{i=1}^{deg[x]} (f[y]+w[x\longrightarrow y]) f[x]=deg[x]1i=1deg[x](f[y]+w[xy]) d e g [ x ] deg[x] deg[x] 就是表示 x x x 的出度, 1 d e g [ x ] \frac{1}{deg[x]} deg[x]1 就是概率
反向建图拓扑排序一遍递推 f [ x ] f[x] f[x] 即可

正推思路:
f [ i ] f[i] f[i] 表示从 1 1 1 i i i 的期望, g [ i ] g[i] g[i] 表示从 1 1 1 i i i 的概率, d e g [ x ] deg[x] deg[x] x x x 的出度,方程的转移:对于一条从 x x x y y y 的边
f [ y ] = 1 d e g [ x ] × ∑ i = 1 i n [ y ] ( f [ x ] + e [ i ] × g [ x ] ) f[y]= \frac{1}{deg[x]} \times \sum_{i=1}^{in[y]}(f[x]+e[i]\times g[x]) f[y]=deg[x]1×i=1in[y](f[x]+e[i]×g[x])
到达 x x x 点路长的期望加上这一步的贡献,乘以可以转移过来的概率,累加所有可以转移到 y y y 点的边即可
这一步的贡献是边长乘以有机会从 x x x 出发选这条边的概率

逆推code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
queue <int> q;
struct Edge{
	int next, to, w;
}e[maxn];
int head[maxn], cnt, in[maxn], deg[maxn];
void add(int x, int y, int z){
	e[++cnt].w = z;
	e[cnt].to = y;
	e[cnt].next = head[x];
	head[x] = cnt;
}
double f[maxn];
void topsort()
{
	q.push(n);
	while(!q.empty())
	{
		int x = q.front();q.pop();
		for(int i = head[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			in[y]--;
			if(!in[y])q.push(y);
			f[y] += (1.0 * f[x] + e[i].w) / deg[y];
		}
	}
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= m; ++i){
		int x, y, z;cin >> x >> y >> z;
		add(y, x, z);
		in[x]++,deg[x]++;
	}
	topsort();
	cout << fixed << setprecision(2) << f[1];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

正推code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
queue <int> q;
struct Edge{
	int next, to, w;
}e[maxn];
int head[maxn], cnt, in[maxn], deg[maxn];
void add(int x, int y, int z){
	e[++cnt].w = z;
	e[cnt].to = y;
	e[cnt].next = head[x];
	head[x] = cnt;
}
double f[maxn], g[maxn];
void topsort()
{
	for(int i = 1; i <= n; ++i) if(!in[i]) q.push(i);
	g[1] = 1;
	while(!q.empty())
	{
		int x = q.front();q.pop();
		for(int i = head[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			in[y]--;
			if(!in[y])q.push(y);
			f[y] += (1.0 * f[x] + e[i].w * g[x]) / deg[x];
			g[y] += g[x] / (1.0 * deg[x]);
		}
	}
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= m; ++i){
		int x, y, z;cin >> x >> y >> z;
		add(x, y, z);
		in[y]++,deg[x]++;
	}
	topsort();
	cout << fixed << setprecision(2) << f[n];
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

提高

待补…
P5569 [SDOI2008] 石子合并
石子合并的加强版(Hard
题意:
一行 n n n 堆石子,每次只能选相邻两堆进行合并,合并后的石子数计为这次的得分,求最后只由一堆石子时的最大得分。

树形dp

P2279 [HNOI2003]消防局的设立

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值