洛谷题单题解1:疯狂A题训练——DP基础篇

题目顺序按难度排序过后

入门到普及:

1.最长连号

  本来第一眼是一个模拟,想一下好像可以dp[i]代表以i为结尾的最长连号(要是用dp的话),以此来进行转移,是一个一眼题

2.P1049 [NOIP2001 普及组] 装箱问题

我们考虑背包,将体积同时也表示为价值,所以此时我们考虑01背包的最大价值就好了

3.P1048 [NOIP2005 普及组] 采药

比较明显的背包问题,我们将时间看为容量,跑01背包就好了

4.P1002 [NOIP2002 普及组] 过河卒

我们将马儿可以跳到的点标记为不能经过,然后按dp[i][j]代表到达(i,j)方案数的总和来转移

是一道主要入门动态规划就会的题

(由于之前没写过现在写一遍)

#include
#include
using namespace std;
#define int long long 
const int maxn = 1e2 + 10;
int dp[maxn][maxn];
int dx[8] = { -2,-2,-1,-1,2,2,1,1};
int dy[8] = { 1,-1,2,-2,1,-1,2,-2 };
int x, y, hx, hy;
bool vis[maxn][maxn];
signed main()
{
	cin >> x >> y >> hx >> hy;
	dp[0][0] = 1;
	vis[hx][hy] = 1;
	for (int i = 0; i < 8; i++)
	{
		int hxx = hx + dx[i], hyy = hy + dy[i];
		if (hxx >= 0 && hyy >= 0)
		{
			vis[hxx][hyy] = 1;//代表不能走
	//		cout << hxx << ' ' << hyy << endl;
		}
	}
	for (int i = 0; i <=x; i++)
	{
		for (int j = 0; j <= y; j++)
		{
			if (i == 0 && j == 0)
				continue;
			else
			{
				if (i != 0)
					dp[i][j] += dp[i - 1][j];
				if (j != 0)
					dp[i][j] += dp[i][j - 1];
				if (vis[i][j])
					dp[i][j] = 0;
			}
		}
	}
	cout << dp[x][y];
}

5.P1510 精卫填海

可以明确是01背包问题,接下来就是如何处理表达式了,我本来是想dp[i]表达为i重量下要的最小体积来转移,但仔细思考发现,我们不太好转移,因为此时dp[v]要表达为大于等于v体积下最小的的体力值,虽然大于=可以利用类似小白月赛中的一道题,单独划分出来判断,不过那样写bug可能比较多也比较难写,所以我就直接利用dp[i]表达为以i为体积最多的转移重量,这与01背包的最基础表达式子符合

#include
#include
using namespace std;
#define int long long
const int maxn = 1e5 + 10;
int dp[maxn];//代表dp[i],i体力最多可以搬动多大的体积
int v, n, c;
int a[maxn], b[maxn];
signed main()
{
	cin >> v >> n >> c;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i] >> b[i];
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = c; j >= b[i]; j--)
		{
			dp[j] = max(dp[j], dp[j - b[i]] + a[i]);
		}
	}
	int ans =-1;
	for (int j = c; j >= 0; j--)
	{
		if (dp[j] >= v)
		{
			ans = c - j;
		}
	}
	if (ans == -1)
	{
		cout << "Impossible";
	}
	else
	{
		cout << ans;
	}
}

 6.P2563 [AHOI2001]质数和分解

可以比较明显的感觉,这是一道多重背包方案数问题,所以我们可以解决了,至于200以内的质数,可以打标(反正没 多少个)我是利用ola筛 ,之后就好做了,风格有些不一样是因为这是我早期写的代码

#include
#include
using namespace std;
bool visit[10010];
int a[100010];
int dp[10010];
void ola(int x)
{
	for (int i = 2; i <= x; i++)
	{
		if (!visit[i])
			a[++a[0]] = i;
		for (int j = 1; j <= a[0] && i*a[j] <= x; j++)
		{
			visit[i*a[j]] = 1;
			if (i%a[j] == 0)
				break;
		}
	}
}
int main()
{
	int t;
	ola(200);
	while (cin >> t)
	{
		memset(dp, 0, sizeof(dp));
		int cnt = 0;
		for (int i = 1; i <= a[0]; i++)
		{
			if (a[i] <= t)
			{
				cnt++;
			}
			else
			{
				break;
			}
		}
		dp[0] = 1;
		for (int i = 1; i <= cnt; i++)
		{
			for (int j = a[i]; j <= t; j++)
			{
				dp[j] = dp[j] + dp[j - a[i]];
			}
		}
		cout << dp[t]<

7.P1616 疯狂的采药

可以发现是多重01背包 比较板

8.P1164 小A点菜

01背包方案数dp,我们可以通过这道题来发现方案数dp的一个特点 就是dp[i]是恰好为i体积的方案数,而不是像一般背包问题那样的最大值之类的表达 也是比较模板的题

9.P1832 A+B Problem(再升级)

  这题起始和第6题一模一样 不管是代码还是思路

10 P1734 最大约数和

我们可以维护出每个数的约数和,将其当作价值,这个数当作体积 通过01背包求解

#include
#include
using namespace std;
int n;
int a[10010];
int dp[10010];
void find(int x)
{
	for (int i = 1; i <= x / 2; i++)
	{
		for (int j = 2; j*i <= n; j++)
		{
			a[j*i] += i;
		}
	}
}
int main()
{
	cin >> n;
	find(n);
	for (int i = 1; i <= n; i++)//本题一共有1-n种方案数
	{
		for (int j = n; j >= i; j--)//每个方案消耗n
		{
			dp[j] = max(dp[j], dp[j - i] + a[i]);//每个方案价值a【n】
		}
	}
	cout << dp[n];
}

11 P2871 [USACO07DEC]Charm Bracelet S

 这道题非常直接 他上来就给你什么是01背包

12 P2639 [USACO09OCT]Bessie's Weight Problem G

 我们将重量当作是体积和价值,跑一遍01背包求最大值

13 P1115 最大子段和

这道题可以用双指针和dp[i],dp[i]表示以i为结尾的最大子序列来转移

    dp[i] = max(a[i], dp[i - 1] + a[i]);

14 P1176 路径计数2

这题和前面跳马的那题一模一样(几乎)

15 P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles

和跳马的转移方程差不多

普及/提高-

16  [NOIP2008 普及组] 传球游戏

dp 是一个神奇的东西,我们考虑的话只能以尝试的心态 除了一些模板外,基本很少能一眼dp的 这题也是 我们可以将传递次数看为层 每次传递就是由上一层传递下来的 这就将这题变为了像过河卒一样的题 都是由上一层转移到这一层的 不管自是表达不同:

dp[i][j]代表第i次传球在球在第j个人手上的方案数:

dp[i][j]=dp[i-1][j-1]+dp[i-1][j+1];//记得特判以下 1,n(因为是环)

#include
#include
#include
#include
using namespace std;
#define int long long
const int maxn = 50;
int dp[maxn][maxn];//dp[i][j]代表第i个人在第j次传球时收到球的方案数
int n, m;
signed main()
{
	cin >> n >> m;
	dp[0][1] = 1;
	for (int i = 1; i <= m; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			if (j == 1)
			{
				dp[i][1] += dp[i - 1][n];
				dp[i][1] += dp[i - 1][2];
			}
			else if(j!=n)
			{
				dp[i][j] += dp[i - 1][j-1];
				dp[i][j] += dp[i - 1][j+1];
			}
			else
			{
				dp[i][n] += dp[i-1][1];
				dp[i][n] += dp[i-1][n-1];
			}
		}
	}
	cout << dp[m][1];
}

17:

Mashmokh and ACM 

我们还是比较明显的考虑一下dp 因为我们考虑一个数在第i位是多少才符合好序列的话可以发现其只与我们前一个数有关 没有后效性 可以尝试dp

我们设dp[i][j]代表以i为结尾长度为j的方案数;

  dp[i][j]+=dp[k][j-1];//k为i的约数

这里介绍一个枚举约数的写法:

for (int i = 1; i <= 2000; i++)
    {
        for (int j = 1; j*i <= 2000; j++)
        {
            p[i*j].push_back(i);
        }
    }

复杂度应该为nlogn 证明的话好像是用调和级数来证明:不太了解

#include
#include
#include
#include
using namespace std;
#define int long long
const int maxn = 2e3 + 10;
const int mod = 1e9 + 7;
int dp[maxn][maxn];
int n, k;
vectorp[maxn];//先预处理每个数的约数
signed main()
{
	cin >> n >> k;
	for (int i = 1; i <= 2000; i++)
	{
		for (int j = 1; j*i <= 2000; j++)
		{
			p[i*j].push_back(i);
		}
	}
	for (int i = 1; i <= n; i++)dp[i][1] = 1;
	for (int i = 2; i <= k; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			for (auto x : p[j])
				(dp[j][i] += dp[x][i-1])%=mod;
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
		(ans += dp[i][k])%=mod;
	cout << ans;
}

18:[NOIP2012 普及组] 摆花

同样考虑为什么是dp 首先我们对于每一种花都是连续的摆放 所以考虑假如一段区间摆一种花会有什么影响 :对于前面的一些花我们只能利用这种花之前的来摆放 对于后面没有影响 所以无后效性(可能讲的不明不白,不过线性dp就是这样比较离谱 练多了就可以发现了)

dp[i][j]代表的是前i盆利用前j种花的摆放种类

dp[i][j]+=(dp[i-k][j-1]) k代表的是我们第j种摆放的数量

#include
#include
#include
#include
using namespace std;
#define int long long
const int maxn = 1e2 + 10;
const int mod = 1e6 + 7;
int dp[maxn][maxn];
int a[maxn];
int n, m;
signed main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
	}
	for(int i=0;i<=n;i++)
	dp[0][i] = 1;
	for (int i = 1; i <=m; i++)
	{
		for (int j = 1; j <=n; j++)
		{
			for (int k = 0; k <= min(a[j],i); k++)
			{
				(dp[i][j] += dp[i - k][j - 1])%=mod;
			}
		}
	}
	cout << dp[m][n];
}

 19:四方定理

 刚开始看的时候以为就是完全背包 当写完样例都没过 仔细思考发现他有固定大小<=4 我们可以利用这点再开一维

dp[i][j]代表i用j个数表示的方案数 此时dp[i][j]+=dp[i-a[k]][j-1];//a[k]代表k的平方数

#include
#include
#include
#include
using namespace std;
#define int long long
const int maxn = 1010;
int a[maxn];
int dp[34000][5];//代表dp[i][j]代表i用j个数分解可以由多少中种方法
signed main()
{
	for (int i = 1; i <= 1000; i++)
		a[i] = i * i;
	dp[0][0] = 1;
	for (int i = 1; i <=1000; i++)
	{
		if (a[i] > 32768)
			break;
		for (int j = a[i]; j <= 33000; j++)
		{
			for(int k=1;k<=4;k++)
			dp[j][k]+= dp[j - a[i]][k-1];
		}
	}
	int t;
	cin >> t;
	while (t--)
	{
		int n;
		cin >> n;
		cout << dp[n][1]+dp[n][2]+dp[n][3]+dp[n][4] << endl;
	}
}

 20  [NOIP2004 提高组] 合唱队形

比较明显的求一个点为中心左边最长的上升子序列,右边最长的下降子序列(都要包括这个点)

比较套路的一种dp

#include
#include
using namespace std;
int dp1[10010], dp2[100010];
int a[100100], b[100100];
int main()
{
    int n;
    cin >> n;
	int cnt = n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		b[cnt--] = a[i];
	}
	for (int i = 1; i <= n; i++)
	{
		dp1[i] = 1;
		dp2[i] = 1;
	}
	for (int i = 2; i <= n; i++)
	{
		for (int j = 1; j < i; j++)
		{
			if (a[j]

21 

删数

这道题还是比较好发现是区间dp的 转移方程也比较好写:

dp[i][j]=max(abs(a[i]-a[j])*(j-i+1),max(dp[i][k]+dp[k+1][j]):k属于 i-j-1)

#include
#include
#include
using namespace std;
#define int long long
const int maxn = 1e2 + 10;
int dp[maxn][maxn];
int a[maxn], n;
signed main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= n; i++)
		dp[i][i] = a[i];
	for (int len = 2; len <= n; len++)
	{
		for (int begin = 1; begin <= n - len + 1; begin++)
		{
			int end = begin + len - 1;
			dp[begin][end] = abs(a[begin] - a[end])*len;
			for (int k = begin; k < end; k++)
			{
				dp[begin][end] = max(dp[begin][end], dp[begin][k] + dp[k + 1][end]);
			}
		}
	}
	cout << dp[1][n];
}

22 :

最大子树和

比较明显的树形dp  主要是转移方程 dp[i]代表以i为子树的最大子树和

转移方程:

 if (dp[y] >= 0)
            dp[u] += dp[y];  y为u的子树 这其中包含一些贪心的思想 对于一个dp[y]>0的子树我们可以将其直接加进来 

#include
#include
using namespace std;
#define int long long
const int maxn = 16100;
int dp[maxn];
struct node
{
	int to, next;
}e[maxn<<1];
int a[maxn],n;
int head[maxn], cnt;
void add(int u, int v)
{
	e[++cnt].next = head[u];
	e[cnt].to = v;
	head[u] = cnt;
}
void dfs(int u, int fa)
{
	dp[u] = a[u];
	for (int i = head[u]; i; i = e[i].next)
	{
		int y = e[i].to;
		if (y == fa)continue;
		dfs(y, u);
		if (dp[y] >= 0)
			dp[u] += dp[y];
	}
}
signed main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i > a1 >> b;
		add(a1, b), add(b, a1);
	}
	dfs(1, 0);
	int ans =-1e9;
	for (int i = 1; i <= n; i++)
	{
		ans = max(ans, dp[i]);
	//	cout << dp[i] << endl;
	}cout << ans;
}

23:最大正方形:

我们考虑遍历顺序对我们更新答案的影响:假设(1,1)为左上角 (n,m)为右下角 此时我们遍历的话是从左往右 从上到下 此时我们可以发现 假设dp[i][j]代表的是以(i,j)为右下角的话最大的正方形的话 我们可以很好的表达出转移方程:

if(a[i][j]==1)

    dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j-1], dp[i][j -1])) + 1;

#include
#include
using namespace std;
#define int long long
const int maxn = 1e2 + 10;
int dp[maxn][maxn];
int a[maxn][maxn];
signed 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];
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)
		{
			if (a[i][j])
			{
				dp[i][j] = min(dp[i - 1][j], min(dp[i - 1][j-1], dp[i][j -1])) + 1;
				ans = max(dp[i][j], ans);
			}
		}
	}
	cout << ans;
}

 24:

最大正方形II

我们将其与上一题对比 只要在上一题基础上加一些条件就好:

if (a[i][j] == 1)
            {
                if (a[i - 1][j - 1] == 1 && a[i - 1][j] == 0 && a[i][j - 1] == 0)
                    dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
                else
                        dp[i][j] = 1;
                ans = max(ans, dp[i][j]);
            }
            else
            {
                if (a[i - 1][j - 1] == 0 && a[i - 1][j] == 1 && a[i][j - 1] == 1)
                    dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
                else
                    dp[i][j] = 1;
                ans = max(ans, dp[i][j]);
            }

#include
#include
using namespace std;
int n, m;
int dp[110][110];
int a[110][110];
int main()
{
	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] == 1)
			{
				if (a[i - 1][j - 1] == 1 && a[i - 1][j] == 0 && a[i][j - 1] == 0)
					dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
				else
						dp[i][j] = 1;
				ans = max(ans, dp[i][j]);
			}
			else
			{
				if (a[i - 1][j - 1] == 0 && a[i - 1][j] == 1 && a[i][j - 1] == 1)
					dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
				else
					dp[i][j] = 1;
				ans = max(ans, dp[i][j]);
			}
		}
	}
	cout << ans;

}

25:

[HAOI2009]逆序对数列

可以比较好写出dp的转移方程和如何转移当时在优化上并没有很熟练 

1. 我们考虑dp[i][j]代表前i个数有j个逆序对的方案数 

可以发现i 与i-1 之间的关系只要在于 插入i时候对其造成的影响:由于i为此时最大值 我们可以发现 不管i插到哪里 我们i后面的数都可以和他构成逆序对 前面的没有影响 这个就给了我们dp的启发:

dp[0][0] = 1;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j <=k; j++)
		{
			for (int j1 = 0; j1 <=min(i-1,j); j1++)//枚举这一位数的贡献
				(dp[i][j] += dp[i - 1][j - j1])%=mod;
		}
	}

这个是3层循环的dp 会t掉

不过我们可以尝试将其优化为二维的 因为 此时我们dp[i][j]+sum(dp[i-1][j(0-j)])

所以此时我们可以考虑维护一个sum 记录一下此时的dp[i-1][j] 利用前缀和的思想来实现

这种优化方式值得记录:

#include
#include
using namespace std;
#define int long long
int n, k;
int dp[1100][1100];//dp[i][j]代表i的全排列有j个逆序对的个数
//状态转移:dp[i][j]=sum(dp[i-1][k]):k属于max(0,j-i+1)
//对于一个i的全排列:最多有:i*(i-1)/2个逆序对:
//对于状态转移:我们将其视为将i插入i-1个数中
int sum;
signed main()
{
	cin >> n >> k;
	dp[0][0] = 1;
	sum = 0;
	for (int i = 1; i <= n; i++)
	{
		sum = 0;
		for (int j = 0; j <= k; j++)
		{
			sum += dp[i - 1][j];
			sum %= 10000;
			dp[i][j] = sum%10000;
			if (j >=i-1)
			{
				sum -= dp[i - 1][j - i + 1];
				sum += 10000;
				sum %= 10000;	
			}
			/*我们考虑jk循环的意义:
			将小等于j的数全部加到dp[i][j]中
			所以我们维护一个sum
			sum跟随j变化:
			但是要考虑j-i+1的情况
			对于此时j-i+1>=0此时我们sum要将其减去?
			因为对于后面的j来讲,这个值是不需要考虑的,所以我们也用线性来改变他

			*/
		}
	}
	cout << dp[n][k];
}

26:

[NOIP1999 普及组] 导弹拦截

这道题是明显的dp 为什么呢 因为求最大可以拦截的数量 每一次拦截又必须比前一次小 这个 就是找长下降子序列就好了(经典问题) 然后一段区间最多可以要多少个系统 我们可以考虑 贪心 每次找最长下降子序列 将其分为一个系统内 那么考虑可能出现更小的吗:假设我们将此时的一个导弹从原本区间中拿出来 可以他会导致什么呢 

1 对原本的区间没有影响

2 假如他可以插到其他区间的话 会导致区间被划分为两个区间 或不变

从上述角度来考虑可以发现要么不变要么变大 所以此时一定为最小的

#include
#include
using namespace std;
int dp[1001000], dp1[1001000];
int a[100100], b[100100];
int main()
{
	int n=0;
	while (cin >> a[++n]);
	n--;
	int cnt = n;
	for (int i = 1; i <= n; i++)
	{
		b[cnt--] = a[i];
	}
	int len1 = 0, len2 = 0;
	for (int i = 1; i <= n; i++)
	{
		if (a[i] > dp[len1])dp[++len1] = a[i];
		else
		{
			int jk = lower_bound(dp + 1, dp + 1 + len1, a[i]) - dp;
			dp[jk] = a[i];
		}
		if (b[i] >= dp1[len2])dp1[++len2] = b[i];
		else
		{
			int jk = upper_bound(dp1 + 1, dp1 + 1 + len2,b[i]) - dp1;
			dp1[jk] = b[i];
		}
	}
	cout << len2 << endl;
	cout << len1;

}

27:友好城市:

1.我们考虑将一边按顺序排列 可以发现假如这个城市的航道是合法的话 他后面的城市对因的另一边城市一定比他对应的城市大即 i

#include
#include
using namespace std;
const int maxn = 1e6 + 10;
struct node
{
	int x, y;
}a[maxn];
bool cmp(node a, node b)
{
	return a.x < b.x;
}
int p[maxn];
int len = 0;
int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i].x >> a[i].y;
	sort(a + 1, a + 1 + n, cmp);
	for (int i = 1; i <= n; i++)
	{
		if (a[i].y > p[len])p[++len] = a[i].y;
		else
		{
			int jk = lower_bound(p + 1, p + 1 + len, a[i].y) - p;
			p[jk] = a[i].y;
		}
	}
	cout << len;
}

28: 食物链:

我们可以比较明确的发现这道题和拓扑排序有关 是一种拓扑dp 我们可以假设入度为0 的点dp[i]=1;

最后是要求入度不为0且出度为0的点都所有dp和

考虑如何转移:



dp[i]+=dp[j];//j->i (代表此时j有一条边连向i)

 29 最大食物链计数 

29 28 两题的思路基本一样 不管转移题目的条件可能不同

我只放一份代码:(28)

#include
#include
#include
using namespace std;
const int maxn = 1e5 + 1e4;
int head[maxn], cnt, num[maxn];
int n, m, u, v, in[maxn], out[maxn];
struct edge
{
	int to, next;
}e[maxn << 3];
int inv[maxn];
void add(int u, int v)
{
	e[++cnt].next = head[u];
	head[u] = cnt;
	e[cnt].to = v;
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		cin >> u >> v;
		add(u, v);
		in[v]++;
		out[u]++;
		inv[v]++;
	}
	stacks;
	for (int i = 1; i <= n; i++)
		if (!in[i]&&out[i])
		{
			s.push(i);
			num[i] = 1;
		}
	while (!s.empty())
	{
		int x = s.top();
		s.pop();
		for (int i = head[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			num[y] += num[x];
			in[y]--;
			if (in[y] == 0)
			{
				s.push(y);
			}
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
	{
		if (!out[i]&&inv[i])
		{
			ans += num[i];
		}
	}
	cout << ans;
}

30:

[NOIP2000 提高组] 乘积最大

一开始想的是区间dp  dp[i][j][k]代表[i,j]划分为k段的最大值

我们按正常区间那样头两维是长度和开头   第3维枚举此时的k 代表改区间划分为几部分  

第4维代表此时区间中界限 即在哪里划分 第5维代表此时左边划分的区间维度

我看评论区还真有人这么写了 做法应该是对的 不管高精度让人不太想写

那考虑线性dp如何写:

1.dp[i][j]代表前i个数被分为j个区间的最大值 

dp[i][j]=max(dp[i][j],dp[i-k][j-1]*a[i-k+1][j]) k代表此时第j个区间的长度

正确性证明:为什么我们不需要考虑此时后面的数呢 因为最后结果为dp[n][k] 是由前面

dp[n-len1][k-1]*a[n-len1+1][n]得到了 那么我们可以发现这个式子仅与前面有关 即无后效性

所以是正确的

31:

[NOIP2003 提高组] 加分二叉树

这道题本来是一个正常的树形dp 结果他这一个中序遍历是1-n给我整不会 看题解才知道这个代表此时我们可以将其看作一条线段从1-n(也是一个知识点吧)

但这玩意起始真的可以写为树形dp 就是在我们记忆化搜索的时候同时进行dp:

主要是利用中序遍历的特点:根左右 代表我们必须先确定一个根并在此基础上进行递归

#include
#include
using namespace std;
#define int long long
const int maxn = 35;
int n;
int a[maxn];
int dp[maxn][maxn];
int root[maxn][maxn];
void print(int l, int r)
{
	if (root[l][r] == 0)
		return;
	cout << root[l][r]<<' ';
	if (l == r)
		return;
	print(l, root[l][r] - 1);
	print(root[l][r] + 1, r);
}
signed main()
{
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= n; i++)
	{
		dp[i][i] = a[i];
		root[i][i] = i;
	}
	for (int len = 2; len <= n; len++)
	{
		for (int begin = 1; begin <= n - len + 1;begin++)
		{
			int end = begin + len - 1;
			dp[begin][end] = dp[begin + 1][end] + a[begin];
			root[begin][end] = begin;
			for (int k = begin+1; k <= end-1; k++)
			{
				int cnt = dp[begin][end];
				dp[begin][end] = max(dp[begin][k-1] * dp[k + 1][end] + a[k], dp[begin][end]);
				if (dp[begin][end] > cnt)
					root[begin][end] = k;
			}
		}
	}
	
	cout << dp[1][n] << endl;
	print(1, n);
}

记忆化搜索版本:

#include
#include
#include
using namespace std;
#define int long long
const int maxn = 1e2;
int dp[maxn][maxn];
int root[maxn][maxn];
int a[maxn];
int dfs(int l, int r)//代表此时的区间范围
{
	if (l > r)
		return 1;
	if (l == r)
	{
		dp[l][r] =a[l];
		root[l][r] = l;
		return dp[l][r];
	}
	if (dp[l][r])return dp[l][r];
	for (int i = l; i <=r; i++)
	{
		int now = dfs(l,i-1)*dfs(i+1, r) + a[i];
		if (now >dp[l][r])
		{
			dp[l][r] = now;
			root[l][r] = i;
	     }
	}
	return dp[l][r];
}
void dfs1(int l, int r)
{
	if (root[l][r] == 0)return;
	cout << root[l][r] << ' ';
	if (l == r)return;
	dfs1(l, root[l][r] - 1);
	dfs1(root[l][r] + 1, r);
}
signed main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)cin >> a[i];
	dfs(1, n);
	cout << dp[1][n]<

32:没有上司的舞会:

对于这道题 他虽然是树形dp的入门题 当其中的套路还是比较好用的很多题都是这个套路:

可以在做完这道题后写:Problem - C - Codeforces

他的重点在于我们假设一颗以u为根节点的树 那么我们考虑这个根节点时 他的状态非常重要 根据题目来说他有来和不来两种状态 所以此时我们分别考虑这两种状态

dp[u][1]代表此时我们考虑以u为根节点的子树 u来的情况 

dp[u][1]代表此时我们考虑以u为根节点的子树 u不来的情况:

dp[u][1]+=dp[y][0];

dp[u][0]+=max(dp[y][1],dp[y][0]);

#include
#include
#include
using namespace std;
const int maxn = 6e3 + 10;
int dp[maxn][2];
int a[maxn],head[maxn],cnt;
struct node
{
	int to, next;
}e[maxn<<1];
void add(int u, int v)
{
	e[++cnt].next = head[u];
	e[cnt].to = v;
	head[u] = cnt;
}
void dfs(int u,int fa)
{
	dp[u][1] = a[u];
	for (int i = head[u]; i; i = e[i].next)
	{
		int y = e[i].to;
		if (y == fa)
			continue;
		else
		{
			dfs(y, u);
			dp[u][0] += max(dp[y][1], dp[y][0]);
			dp[u][1] += dp[y][0];
		}
	}
}
int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <=n-1; i++)
	{
		int x, y;
		cin >> x >> y;
		add(x, y);
		add(y, x);
	}
	dfs(1, -1);
	cout << max(dp[1][0], dp[1][1]);
}

33:

[NOIP2014 提高组] 联合权值

这道题只要对所有值取模而不是都取摸

思路在于 我们假如要求所有距离为2的话直接遍历以每个为根他的所有兄弟就好了 那么要是祖先的情况怎么办 我们考虑*2 将祖先的情况也一并考虑:建议画图理解

#include
#include
using namespace std;
#define int long long
const int maxn = 2e5 + 10;
const int mod = 1e4 + 7;
struct node
{
	int to, next;
}e[maxn<<1];
int head[maxn],w[maxn],cnt;
void add(int u, int v)
{
	e[++cnt].next = head[u];
	e[cnt].to = v;
	head[u] = cnt;
}
signed  main()
{
	int n;
	cin >> n;
	for (int i = 1; i > a >> b;
		add(a, b), add(b, a);
	}
	for (int i = 1; i <= n; i++)cin >> w[i];
	int ans1 = 0, ans2 = 0;
	for (int u = 1; u <= n; u++)
	{
		int now1 = 0,now2=0,now3=0;
		for (int i = head[u]; i; i = e[i].next)
		{
			ans2 += 2*now1*w[e[i].to];
			ans2 %= mod;
			now1 += w[e[i].to];
			if (w[e[i].to] > now2){
			now3 = now2;
			now2 = w[e[i].to];	
			}
			else if (now3 < w[e[i].to])
			{
				now3 = w[e[i].to];
			}
		}
	//	cout << now2 << ' ' << now3 << endl;
		ans1 = max(ans1, (now2*now3));
	}
	cout << ans1 << ' ' << ans2;
}

34:

[BJWC2008]雷涛的小猫

比较明显的dp 我们考虑dp[i][j]代表在高度为i的第j棵时我们可以获得的最大价值,转移方程比较好写当处理delta比较难 由于delta转移时比较固定 就是从高度为i-delta的最大值跳过来 那么我们比较容易想到此时维护一个maxx代表每一层最大的值 这下转移变为0(1)了

#include
#include
using namespace std;
const int maxn = 2100;
int dp[maxn][maxn];
int a[maxn][maxn];
int n, h,delta;
int maxx[maxn];//每一层的最大值
int main()
{
	cin >> n >> h>>delta;
	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]++;
		}
    }
	for (int i = 1; i <= n; i++)
	{
		dp[1][i] = a[1][i];
		maxx[1] = max(maxx[1], dp[1][i]);
	}
	for (int i = 2; i <= h; i++)//高度
	{
		for (int j = 1; j <= n; j++)
		{
			dp[i][j] = dp[i-1][j]+a[i][j];
			if (i >delta)
				dp[i][j] = max(dp[i][j], maxx[i - delta]+a[i][j]);
			maxx[i] = max(maxx[i],dp[i][j]);
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
	{
		ans = max(ans, dp[h][i]);
	}
	cout << ans;
}

  35:玩具取名:

1.首先2个字母比较关键 因为这确定了我们每个区间最多要维护为1个字母就够了

原因在于 假如我们枚举区间分界点 将这个区间划分为两个再合并的话

考虑最后一个[1,n]的区间 此时我们维护为1个字符的话必须由两个字符变化而来 这代表我们此时 最多将这个区间变为1个

所以考虑dp[l][r][i]代表将区间[l,r]变为i是否可能 接下来就是普通的区间dp了

#include
#include
#include
using namespace std;
int res[10][10][10];
int ans[210][210][5];
int change(char a)
{
	if (a == 'W')
	{
		return 1;
	}
	if (a == 'I')
		return 2;
	if (a == 'N')
		return 3;
	
	return 4;
}
int main()
{
	int cnt[5];
	for (int i = 1; i <= 4; i++)
		cin >> cnt[i];
	for (int i = 1; i <= 4; i++)
	{
		for (int j = 1; j <= cnt[i]; j++)
		{
			string s;
			cin >> s;
			int jk1 = change(s[0]),jk2=change(s[1]);
			res[jk1][jk2][i] = 1;
		}
	}
	string s;
	cin >> s;
	int n = s.length();
	for (int i = 1; i <= n; i++)
		ans[i][i][change(s[i-1])] = 1;
	for (int len = 2; len <= n; len++)
	{
		for (int begin = 1; begin <= n - len+1; begin++)
		{
			int end = begin + len - 1;
			for (int x = 1; x <= 4; x++)
			{
				for (int y = 1; y <= 4; y++)
				{
					for (int z = 1; z <= 4; z++)
					{
						for (int k = begin; k <= end-1; k++)
						{
							if (ans[begin][k][x] && ans[k + 1][end][y] && res[x][y][z])
							{
								ans[begin][end][z] = 1;
								break;
							}
						}
					}
				}
			}
		}
	}
	bool flag = 0;
	if (ans[1][n][1]) flag = 1, printf("%c", 'W');
	if (ans[1][n][2]) flag = 1, printf("%c", 'I');
	if (ans[1][n][3]) flag = 1, printf("%c", 'N');
	if (ans[1][n][4]) flag = 1, printf("%c", 'G');
	if (!flag) puts("The name is wrong!");
	return 0;
}

CF2B

早年cf题 看了半天仍是没看出来10=2x5 。。。。。 数学果然不太行

p1280:

是一个非常好的题目 这道题教会了我几个道理:

1.dp的本质就是优化的暴力 

2.dp的无后效性的理解

这道题的做法在于首先我们确定无法从1-n开始dp(我不会)因为此时我们会发现假如正走的话会导致受到之后情况的影响 那么此时我们只要从n->1就可以有效的避免后效性了 

然后就和我们正常dp一样了

#include
#include
#include
using namespace std;
const int maxn = 1e4 + 10;
int cnt[maxn];
int dp[maxn];
struct node
{
	int id, last;
}e[maxn];
signed main()
{
	int n, k;
	cin >> n >> k;
	for (int i = 1; i <= k; i++)
	{
		cin >> e[i].id >> e[i].last;
		cnt[e[i].id]++;
	}
	for (int i = n; i >= 1; i--)
	{
		if (cnt[i] == 0)
		{
			dp[i] = dp[i + 1] + 1;
		}
		else
		{
			for (int j = 1; j <= k; j++)
			{
				if (e[j].id == i)
				{
					dp[i] = max(dp[i], dp[i + e[j].last]);
				}
			}
		}
	}
	cout << dp[1];
}

p1279:

感觉快抓住dp的精髓了

很明显我们大概率是设二维dp[i][j];

现在考虑如何转化

因为我们二维dp[i][j]代表的就是将前i个a和前j个b维护为等长的情况 此时再转移的话就是 

类似的二维dp一般可以通过dp[i-1][j-1],dp[i-1][j],dp[i][j-1]来转移

那么我们此时首先可以确定的是:dp[i-1][j-1]+abs(a[i]-b[j])

对于后两种 因为此时i-1,j等长 那么i就是多余的 就要利用k来维护

#include
#include
#include
using namespace std;
#define int long long
const int maxn = 2e3 + 10;
int dp[maxn][maxn];
string s1, s2;
signed main()
{
	cin >> s1 >> s2;
	int len1 = s1.size();
	int len2 = s2.size();
	s1 = '`' + s1;
	s2 = '`' + s2;
	int k;
	cin >> k;
	memset(dp, 0x3f, sizeof dp);
	dp[0][0] = 0;
	for (int i = 1; i <= len1; i++)
	{
		dp[i][0] = i * k;
	}
	for (int i = 1; i <= len2; i++)
	{
		dp[0][i] = i * k;
	}
	for (int i = 1; i <= len1; i++)
	{
		for (int j = 1; j <= len2; j++)
		{
			dp[i][j] = min(dp[i - 1][j - 1] + abs(s1[i] - s2[j]), min(dp[i - 1][j] + k, dp[i][j - 1] + k));
		}
	}
	cout << dp[len1][len2];
}

p1005 

很好 高精度 不写(改__int128 就好了)

主要是这个思想比较好实现 洛谷有一道基本一样的题目

可以比较明确区间dp:(比较数据范围这么小)

对于每一个行都是独立的:此时我们考虑dp[i][j]代表此时区间[i,j]在次数[n-len+1,n]时的最大值

因为我们可以模拟一下可以发现要想第一次是开头或结尾只有这种办法了

#include
#include
#include
using namespace std;
#define int long long
const int maxn = 100;
__int128 dp[maxn][maxn];//第i行的情况
__int128 ans = 0;
int a[maxn];
inline void print(__int128 x) {
	if (x < 0) { x = -x; putchar('-'); }
	if (x > 9) print(x / 10);
	putchar(x % 10 + '0');
}
int q_mul(int a, int b)
{
	int res = 1;
	while (b)
	{
		if (b & 1)
		{
			res = res * a;
		}
		b >>= 1;
		a = a * a;
	}
	return res;
}
signed main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)cin >> a[j];
		memset(dp, 0, sizeof dp);
		for (int j = 1; j <= m; j++) {
			dp[j][j] = a[j] * q_mul(1ll * 2, m);
		}
		for (int len = 2; len <= m; len++)
		{
			for (int begin = 1; begin <= m - len + 1; begin++)
			{
				int end = begin + len - 1;
				dp[begin][end] = max(dp[begin + 1][end] + a[begin] * q_mul(1ll * 2, m - len + 1), dp[begin][end - 1] + a[end] * q_mul(1ll * 2, m - len + 1));
			}
		}
		ans += dp[1][m];
	}
     print(ans);
}

CF607A

说实话我感觉这不是dp 这就是一道模拟题

因为对于每个防御塔可以发射的距离固定且情况固定 根本没有dp的思想

如何处理加一个防御塔的情况就枚举他可以破坏道哪里就好了

硬要说dp的话只有后效性这方面有挂钩吧

#include
#include
using namespace std;
#define int long long
const int maxn = 1e5 + 10;
int dp[maxn];
struct node
{
	int x, len;
	inline bool operator<(const node&t)const
	{
		return x < t.x;
	}
}e[maxn];
int cf[maxn];
signed main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> e[i].x >> e[i].len;
	}
	sort(e + 1, e + 1 + n);
	//
	for (int i = 1; i <= n; i++)
		cf[i] = e[i].x;
	for (int i = 1; i <= n; i++)
	{
		int now = e[i].x -e[i].len;//此时为毁掉这中间的所有
		int num = lower_bound(cf+ 1, cf+ 1 + n, now) -cf;
		num--;
		if (num == 0)
		{
			dp[i] =i-1;
		}
		else
		{
			dp[i] = dp[num] + i - num-1;
		}
	}
	int ans = 1e9;
	for (int i = 1; i <= n; i++)
	{
		ans = min(ans, dp[i] + n - i);
	}
	cout << ans;
}

p2602 数位dp不在练习范围内

p2017

 这道题不是dp。。 虽然但是这道题的解法是特别优秀的可能对于很多大佬来说并不这么想但对于我们这些基础不扎实的人来说确实是非常不错的题目:

具体思路如下:

1.因为这道题是求一个DAG ”众所周知“ 拓扑排序可以求一个排序:有向无环图的排序 这个排序的优势在于从前面往后面经行连边的话不会影响我们的拓扑关系 即不可能生成一个环

原因在于假如连接i->j会生成环的话 我们这条边一定会产生一个问题即in[i]无法到0

但此时我们仅改变这个点的出度与入度无关所以无法产生环

#include
#include
#include
using namespace std;
#define int long long 
const int maxn = 1e5 + 10;
int in[maxn];
int topo[maxn];
struct node
{
	int to, next;
}e[maxn];
pairp[maxn];
int head[maxn], cnt;
void add(int u, int v)
{
	e[++cnt].next = head[u];
	e[cnt].to = v;
	head[u] = cnt;
}
int n,m;
void tp()
{
	queueq;
	for (int i = 1; i <= n; i++)
	{
		if (!in[i])
			q.push(i);
	}
	int now = 0;
	while (!q.empty())
	{
		int x = q.front();
		q.pop();
		topo[++now] = x;
		for (int i = head[x]; i; i = e[i].next)
		{
			int y = e[i].to;
			in[y]--;
			if (!in[y])
			{
				q.push(y);
			}
		}
	}
}
int ans[maxn];
signed main()
{
	int p1, p2;
	cin >> n >> p1 >> p2;
	for (int i = 1; i <= p1; i++)
	{
		int x, y; cin >> x >> y;
		add(x, y);
		in[y]++;
	}
	for (int i = 1; i <= p2; i++)
	{
		cin >> p[i].first >> p[i].second;
	}
	tp();
	for (int i = 1; i <= n; i++)
		ans[topo[i]] = i;
	for (int i = 1; i <= p2; i++)
	{
		int x = p[i].first, y = p[i].second;
		if (ans[x] > ans[y])
		{
			swap(x, y);
		}
		cout << x << ' ' << y << endl;
	}
}

你可能感兴趣的:(学习,算法,c++)