DP专题(DP进阶题单 )

(下面的是索引,可点击到指定题目)

书本整理

HDU3466 Proud Merchants背包类\(dp\)

琪露诺(数据结构优化dp or 斜率优化\(dp\))

玩具装箱toy(斜率优化\(dp\))

P2900 [USACO08MAR]Land Acquisition G(斜率优化DP)

牛牛与数组(计数类\(dp\))

逛公园(提高组2017 - Day1T3)(图上\(dp\))

子串(字符串\(dp\))

最长括号匹配(括号匹配问题 + dp)

Harbingers(斜率优化)

CF1092F Tree with Maximum Cost(换根dp)

蜈蚣(经典\(dp\))

[CF1443B] Saving the City(01串dp)

[USACO 10MAR]Great Cow Gathering G

瞅一眼有没有你想看的题目呗。

[Luogu]P1103书本整理

.这道题目是经典的正难则反\(dp\),删去\(k\)本书不大好处理,我们就直接转化为选\(n - k\)本书,这样子就好处理了。
有时候逆向思维确实是很重要的。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105,INF = 10000000;
int n,p,ans = INF;
int dp[MAXN][MAXN];//前i本书挑出j本

struct Node {
	int h,L;
} T[MAXN];

int cmp(Node A,Node B)
{
	return A.h < B.h;
}

int main()
{
	cin >> n >> p ;
	for(int i = 1 ; i <= n ; i ++)cin >> T[i].h >> T[i].L;
	sort(T + 1 , T + 1 + n , cmp);
	for(int i = 1 ; i <= n ; i ++)dp[i][1] = 0;
	for(int i = 1 ; i <= n ; i ++)
		for(int j = 2 ; j <= n ; j ++)
		dp[i][j] = INF;
	for(int i = 1 ; i <= n ; i ++)
	{
		for(int j = 2 ; j <= n - p ; j ++)
		{
			for(int k = 1 ; k <= i - 1 ; k ++)
			dp[i][j] = min(dp[k][j - 1] + abs(T[i].L - T[k].L),dp[i][j]);
		}
	}
	int Ans = INF;
	for(int i = n - p ; i <= n ; i ++)Ans = min(dp[i][n - p],Ans);
	cout << Ans;
	return 0;
}

HDU3466 Proud Merchants

这道题是个经典的问题:背包。

题意简化:
\(n\)(\(n\) ≤ 500) 个物品,第 i 个物品价格为 \(P_i\) ,价值为 \(V_i\) ,
但是只有当你的钱不少于 \(Q_i\) 时才能购买。
给你 \(m\)(\(m\) ≤ 5000) 元钱,求最多能买到的价值和。

做法:按照:\(P_a\) - \(Q_a\) > \(P_b\) - \(Q_b\)排序即可

#include <iostream>
#include <math.h>
#include <algorithm>
using namespace std;
const int MAXN = 5005;
int n , m ,T;

int dp[MAXN];

struct Node {
	int Q,P,V;
} O[MAXN];

int cmp(Node A, Node B)
{
	return A.P - A.Q > B.P - B.Q;
}

int main()
{
	while(scanf("%d%d",&n,&m) != EOF)
	{
		for(int i = 1 ; i <= n ; i ++)
			scanf("%d%d%d",&O[i].P,&O[i].Q,&O[i].V);
		for(int i = 1 ; i <= m ; i ++)dp[i] = 0;
		sort(O + 1 , O + 1 + n , cmp);
		for(int i = 1 ; i <= n ; i ++)
		{
			for(int j = m ; j >= O[i].P ; j --)
			if(j >= O[i].Q)
			dp[j] = max(dp[j],dp[j - O[i].P] + O[i].V);
		}
		printf("%d\n",dp[m]);
	}
	return 0;
}

整数拆分:

\(n\) 拆分为 \(k\) 个不同正整数的和,求方案数。\(k\)\(n\)\(10^5\)

首先考虑暴力。
设置状态:\(dp[i][j]表示i拆分为j个正整数的方案\)
然后推状态转移方程:假设现在是\(dp[i][j]\)(留坑待填)

琪露诺

这道题是优先队列优化\(dp\).但是貌似线段树也可以优化.

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 400005,INF = 2147483649;
int n ;
int dp[2 * MAXN];
int data[MAXN],L,R;

struct Segment {
	int l,r,Max;
} T[MAXN * 8];

void build(int x,int l,int r)
{
	T[x].l = l , T[x].r = r;
	if(l == r){T[x].Max = dp[l];return ;}
	int mid = (l + r) >> 1;
	build(x << 1 , l , mid);
	build(x << 1 | 1 , mid + 1 , r);
	T[x].Max = max(T[x << 1].Max , T[x << 1 | 1].Max);
	return ;
}

void change(int x,int pos,int k)
{
	if(T[x].l == pos && T[x].r == T[x].l)
	{
		T[x].Max = k;
		return ;
	}
	int mid = (T[x].l + T[x].r) >> 1;
	if(pos <= mid)change(x << 1 , pos , k);
	else change(x << 1 | 1 , pos , k );
	T[x].Max = max(T[x << 1].Max , T[x << 1 | 1].Max);
}

int GetMax(int x,int l,int r)
{
	int Max = - INF;
	if(T[x].l >= l && T[x].r <= r)return T[x].Max;
	int mid = (T[x].l + T[x].r) >> 1;
	if(l <= mid)Max = max(Max,GetMax(x << 1 , l,  r));
	if(r  > mid)Max = max(Max,GetMax(x << 1 | 1 , l , r));
	return Max;
}

signed main()
{
	cin >> n >> L >> R;
	for(int i = 0 ; i <= n ; i ++)
		cin >> data[i];
	dp[0] = data[0];
	for(int i = 1 ; i <= n + R; i ++)dp[i] = -INF;
	build(1,0, n + R);
	for(int i = L ; i <= n + R ; i ++)
	{
		int l = max(i - R,0ll) , r = i - L;
		dp[i] = GetMax(1,l,r) + data[i];
		change(1,i,dp[i]);
	}
	int Ans = -INF;
	for(int i = n + 1 ; i <= n + R; i ++)
		Ans = max(dp[i],Ans);
	cout << Ans << endl;
	return 0;
}

斜率优化DP

回到文章顶部

玩具装箱toy

首先我们想不考虑时空消耗,最裸的暴力怎么做?

设立状态:

\(dp[i]\)表示前面的\(i\)件物品装箱需要的最小费用

状态转移:

假设现在是\(dp[i]\),那么对于\(dp[i],枚举从哪一个状态转移过来的,也就是假设从\)dp[j]\(转移过来,那么意味着,前\)j\(个都已经装好,而物品\)j + 1\(到物品\)i\(统统装入一个箱子\)

所以方程会是:\(dp[i] =min(dp[j] + ((\sum_{k = j + 1}^{k = i}{a_k}) - L + i - j - 1)^2)\)

对于求解\((\sum_{k = j + 1}^{k = i}{a_k})\)的过程我们可以用前缀和,那么这样子的时间复杂度就会是\(n^2\),貌似过不了\(n <= 5*10^4\)的凉心数据。

观察时间复杂度的瓶颈,发现是在转移上。考虑如何优化转移,使得转移的时间复杂度\(O(1) 或者 O(log(n))\)

把状态转移方程重新合并同类项后:\(dp[i] =min(dp[j] + ((sum[i] + i) - (sum[j] + j + L + 1))^2)\)

发现一个问题,\(sum[i] + i\)只与\(i\)有关,\(sum[j] + j + L + 1\)只与\(j\)有关,所以我们思考如何去掉取"\(min\)"的过程。

不妨令 \(A = sum[i] + i\),\(B = sum[j] + j + L + 1\) \((j < i)\)

假设现在有一个决策点\(C\),比\(B转移到A更加优秀,写成式子是怎么样的?\)

不妨令\(C = sum[k] + k + L + 1\) \((j < k)\)

(\(你会发现对于dp[i]的最优决策点对于dp[i-1]最优决策点是单调不降的)\)

\(那么 dp[j] + (A - B)^2 > dp[k] + (A - C)^2\)

\(把原式拆开 dp[j] + A^2- 2A*B + B^2 > dp[k] + A^2 - 2*A*C + C^2\)

\(移项合并同类项: dp[j] - dp[k] > 2*A*B - 2*A*C + C^2 - B ^ 2\)

\(再提取公因式: dp[j] - dp[k] > 2*A *(B - C) +(C + B) * (C - B)\)

\(继续合并同类项:dp[j] - dp[k] > (A - C) * (B - C) + (A - B) *(B - C)\)

\(不等式两边同时除以(B - C):\frac{dp[j] - dp[k]}{B-C} > 2*A - B - C\)

最后的式子就会变成:\(\frac{dp[j] + B^2 - dp[k] - C ^ 2}{B-C} > 2*A\)

改一下好看点:\(\frac{(dp[j] + B^2) - (dp[k] + C ^ 2)}{B-C} > 2*A\)

这一个A显然是会单调递增的。重申一下我刚刚干了什么。

我求出了什么情况下会使得决策点\(C比决策点B\)优秀

\(\frac{(dp[j] + B^2) - (dp[k] + C ^ 2)}{B-C} > 2*A\)也就是满足这个式子的时候

这个式子,有点像求斜率的公式:\(k = \frac{△y}{△x}\)

\(B\)看做是\(x1\),\(C\)看做是\(x2\),\(dp[j] + B^2看做y1,dp[k] + C^2看做y2\)

那么想象现在平面上有两个点:\((B,dp[j] + B ^ 2)\) , \((C,dp[k] + C^2)\)

这两个点连成的直线如果斜率大于\(2*A\),那么\(C\)就要比\(B\)更优

初始化我们需要加入一个点:\((0,0)如果选这个点就表示直接当前物品单独成箱最优\)

然后单调队列维护一个下凸包即可。(所有博客都有这一句话,但是都没说怎么维护.......

先是要维护队首,把那一些不是最优决策点的决策点弹掉,然后计算答案,再在队尾维护一下下凸包,怎么维护?

因为是下凸包,你每次会往队尾加入点\(i\),倘若现在队尾与队尾-1那个点组成的直线斜率是大于等于队尾-1的那个点与点\(i\)组成的直线的话就显然不行。

下凸包:往下凸,每一段斜率是单调不降的。

上凸包:往下凹,每一段斜率是单调不升的。

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

long long n,L;

long long sum[100005];
long long Q[100005],dp[100005];


long double X(int x)
{
	return sum[x] + L;//具体参考上面的式子
}

long double Y(int x)
{
	return (long double)(dp[x]) + X(x) * X(x); //具体参考上文的式子
}

long double slope(int x,int y)
{
	return (Y(x) - Y(y)) / (X(x) - X(y));//计算斜率
}

long long sqr(long long x)
{
	return x * x;
}

int main()
{
	cin >> n >> L;L ++;
	for(long long i = 1 ; i <= n ; i ++)
	{
		long long x;
		cin >> x;
		sum[i] += sum[i - 1] + x + 1;//前缀和
	}
	int tail = 1,head = 1;
	Q[tail] = 0ll;
	for(long long i = 1 ; i <= n ; i ++)
	{
		while(head < tail && slope(Q[head],Q[head + 1]) <= (long double)(2 * sum[i]))head ++;//如果不是最优情况就不要,保证队首是最优决策点
		dp[i] = dp[Q[head]] + sqr(sum[i] - (sum[Q[head]] + L));
		while(head < tail && slope(Q[tail - 1],Q[tail]) >= slope(Q[tail-1],i))tail --;//维护 下凸包 的性质
		tail ++ , Q[tail] = i;//入队
	}
	cout << dp[n];
	return 0;
}

回到文章顶部

P2900 [USACO08MAR]Land Acquisition G

题意简化:

给定许多个二元对,你可以把若干个二元对\((x,y)\)归为一类,对于同一组的所有二元对,你需要付出同组内最大\(x\)乘以最大\(y\)的费用,求一种分组使得费用最少。

思路:

先贪心一下,按照\(x\)从小到大排序,\(x\)相同的就按照\(y\)的升序排序,这时候我们会得到一个\(x\)单调上升的二元对序列。

倘若一个点\(i\)它的\(x\)小于等于点\(j\)\(x\)并且它的\(y\)小于等于点\(j\)\(y\),那么很显然,这个点\(i\)就废了,因为把\(i\)\(j\)放一组即可使得\(i\)完全失去对答案贡献的能力///

把那一些“废掉”的点全部删掉,我们得到的就会是一个\(x\)单调上升,\(y\)单调下降的二元组序列。

然后,我们的\(DP\)就出来了

状态设立:\(dp[i]\)表示把前\(i\)个二元组分好组需要的最小花费。

状态转移:\(dp[i] = min(dp[i],dp[j] + x[i] * y[j + 1])(j < i)\)

因为按照前面的排序以及删除元素,序列变为了\(x\)单调上升,\(y\)单调下降的二元组序列,所以直接按照这个方式转移即可。

考虑如何使用大杀器:斜率优化。

这个柿子都不用拆了,直接就已经帮我完成一半的工程了。。

然后思考,假如决策点\(k\)优于决策点\(j\)\(k\)以及\(j\)的关系是怎么样的?

因为决策\(k\)优与决策\(j\),所以:

\(dp[k] + x[i] * y[k + 1] < dp[j] + x[i] * y[j + 1]\)

接着移项。

\(dp[k] - dp[j] < x[i] * (y[j + 1] - y[k + 1])\)

\(\frac{dp[k] - dp[j]}{y[j + 1]-y[k + 1]} < x[i]\)

\(\frac{dp[k] - dp[j]}{y[k + 1]-y[j + 1]} > -x[i]\)
(因为你会发现前面的式子的结果是个负数)

求斜率的公式:\(k = \frac{△y}{△x}\)

那么想象现在平面上有两个点:\((y[k+1],dp[k])\) , \((y[j + 1],dp[j])\)

这两个点连成的直线如果斜率大于\(x[i]\),那么\(k\)就要比\(j\)更优

然后就可以斜率优化了,单调队列维护一个上凸包即可。

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 50005;

int n,N = 0;
int dp[MAXN],Q[MAXN];

struct Node {
	int x,y;
} T[MAXN],New[MAXN];

int cmp(Node A, Node B)
{
	if(A.x != B.x)return A.x < B.x;
	else return A.y < B.y;
}

double sploe(int a,int b)
{
	return (double)(dp[b] - dp[a]) / (double)(T[b + 1].y - T[a + 1].y);
}

signed main()
{
	cin >> n;
	for(int i = 1 ; i <= n ; i ++)
		cin >> T[i].x >> T[i].y;
	sort(T + 1 , T + 1 + n , cmp);
	for(int i = 1 ; i <= n ; i ++)
	{
		if(T[i].y < New[N].y)N ++ , New[N] = T[i];
		else 
		{
			while(N && New[N].y <= T[i].y)N --;
			N ++;
			New[N] = T[i];
		}
	}
    
	n = N;
	for(int i = 1 ; i <= n ; i ++)T[i] = New[i];
	int tail = 1 , head = 1;
	Q[1] = 0;
	for(int i = 1 ; i <= n ; i ++)
	{
		while(head < tail && sploe(Q[head],Q[head + 1]) >= (double) -T[i].x)head ++;//反一下符号
		dp[i] = dp[Q[head]] + T[i].x * T[Q[head] + 1].y;
		while(head < tail && sploe(Q[tail - 1],Q[tail]) >= sploe(Q[tail],i))tail --;
		tail ++ , Q[tail] = i;
	}
	cout << dp[n] << endl;
	return 0;
}

回到文章顶部

计数类DP

牛牛与数组

1:长度为n
2:每一个数都在1到k之间
3:对于任意连续的两个数A,B,A<=B 与(A % B != 0) 两个条件至少成立一个

请问一共有多少满足条件的数组,对1e9+7取模

这是一个计数类DP,下面是\(K^2 * n\)的算法,会T成渣渣.

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 11,MAXM = 100005 , Mod = 1000000007; 

int n ,k ,Ans = 0;
int dp[MAXN][MAXM];//前i个数,最后一个数字为j

signed main()
{
	cin >> n >> k;
	for(int i = 1 ; i <= k ; i ++)dp[1][i] = 1;
	for(int i = 2 ; i <= n ; i ++)
	{
		for(int j = 1 ; j <= k ; j ++)
			for(int l = 1; l <= k ; l ++)
			if(l <= j || l % j != 0)
			dp[i][j] += dp[i - 1][l] , dp[i][j] %= Mod;
	}
	for(int i = 1 ; i <= k ; i ++)Ans += dp[n][i] ,Ans %= Mod;
	cout << Ans % Mod;
	return 0;
}

其实题目只是告诉你,你求的这个数组它的相邻两项不是倍数关系,否则就不行

我们可以进行补集转化,就对于最后一位是\(j\)的状态,我们可以枚举后面接哪一些元素不行。

显然是\(j\)的倍数。那么枚举这个的时间复杂度是多少?

大概是:Θ\((log(k))\),因为\(\sum_{i = 1}^{i = k}{\frac{k}{i}}\)\(log(k)\)

枚举后,我们把整个状态集加上这一个状态的贡献,但是对于不行的那些点就不加,这样子就行了。

时间复杂度为:O\((klog(k) * n)\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MAXN = 11,MAXM = 100005 , Mod = 1000000007; 

int n ,k ,Ans = 0,add = 0;
int dp[MAXN][MAXM];//前i个数,最后一个数字为j,第一维貌似可以滚动掉,没必要就没搞了....
int q[MAXM];//记录哪一些要减去贡献

signed main()
{
	cin >> n >> k;
		for(int i = 1 ; i <= k ; i ++)dp[1][i] = 1;
		for(int i = 1 ; i <= n ; i ++)
		{
			for(int j = 1 ; j <= k ; j ++)
				dp[i][j] += add,dp[i][j] %= Mod,
				dp[i][j] =(dp[i][j] + Mod - q[j] % Mod) % Mod;
			for(int j = 1 ; j <= k ; j ++)q[j] = 0,add = 0;
			for(int j = 1 ; j <= k ; j ++)
			{
				add += dp[i][j];//对于整个状态集加上当前状态的贡献
				for(int l = j * 2 ; l <= k ; l += j)
				q[l] += dp[i][j];//枚举倍数,第l个点减去总贡献累加到q[l]
			}
		}
	for(int i = 1 ; i <= k ; i ++)Ans += dp[n][i] ,Ans %= Mod;//统计答案
	cout << Ans % Mod;
	return 0;
}

回到文章顶部

逛公园(提高组2017 - Day1T3)

题意简化:给定一张有向图(无自环重边),令第1号点到第\(N\)号点的最短路距离为d,现在问从1到\(N\)号点的长度不超过\(d + k\)的路径的条数。答案对P取模。(留坑待填)

字符串DP

回到文章顶部

子串

有两个仅包含小写英文字母的字符串 A 和 B。现在要从字符串 A 中取出 \(k\)互不重叠的非空子串。

然后把这 \(k\) 个子串按照其在字符串 A 中出现的顺序依次连接起来得到一个新的字符串。

请问有多少种方案可以使得这个新串与字符串 B 相等?注意:子串取出的位置不同也认为是不同的方案。

输出答案对 1,000,000,007 取模的结果.

\(|A|\) <= \(10^3\) , \(|B|\) <= \(200\) , \(1\)<= \(k\) <= \(|B|\)

推荐一个博客吧:links

贴个代码跑路了。上面博客讲得太太太太好了!

Code :

#include <bits/stdc++.h>
using namespace std;
#define int long long
int n,m,K;
const int MAXN = 1005,MAXM = 205 , MAXK = 205 ,Mod = 1000000007;
int dp[3][MAXM][MAXK][3];
char A[MAXN],B[MAXM];
signed main()
{
	cin >> n >> m >> K;
	cin >> A + 1 >> B + 1;
	dp[0][0][0][0] = 1;
	dp[1][0][0][0] = 1;//初始化边界
	for(int i = 1 ; i <= n ; i ++)
	{
		for(int j = 1 ; j <= m ; j ++)
		{
			for(int k = 1 ; k <= K ; k ++)
			if(A[i] == B[j])
			{
				dp[i % 2][j][k][1] = dp[(i - 1) % 2][j - 1][k][1] +
				dp[(i - 1) % 2][j - 1][k - 1][1] % Mod + dp[(i - 1) % 2][j - 1][k - 1][0];
				dp[i % 2][j][k][1] %= Mod;
				dp[i % 2][j][k][0] = dp[(i - 1) % 2][j][k][0] + dp[(i - 1) % 2][j][k][1];
				dp[i % 2][j][k][0] %= Mod;
			}
			else 
			{
				dp[i % 2][j][k][1] = 0;
				dp[i % 2][j][k][0] = dp[(i - 1) % 2][j][k][0] + dp[(i - 1) % 2][j][k][1];
				dp[i % 2][j][k][0] %= Mod;
			}
		}
	}
	cout << (dp[n % 2][m][K][0]%Mod + dp[n % 2][m][K][1] % Mod)%Mod;
	return 0;
}

回到文章顶部

最长括号匹配

题意:

给定一个长度<= \(10^6\)的括号序列,含有'[' ,']','(',')'这四种字符,求最长的一段合法括号序列。输出这个括号序列。

思路:

思路一:
用栈进行括号匹配,如果成功匹配,就在一个桶里面打上标记,\(vis[i] = 1\),最后找一遍最长的,\(vis[i]\)都为1的子段就行了。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 50;
char s[MAXN];
int top = 0,tack[MAXN],sum[MAXN];
bool vis[MAXN];
int flag = 1;

bool pd(int x,int y)
{
	if(s[x] == '[' && s[y] == ']')return 1;
	if(s[x] == '(' && s[y] == ')')return 1;
	return 0;
}

int main()
{	
	cin >> s + 1;
	int len = strlen(s + 1);
	for(int i = 1 ; i <= len; i ++)
	{
		if(s[i] == '(' || s[i] == '[')
			top ++,tack[top] = i;
		else 
		{
			if(pd(tack[top],i))vis[tack[top]] = vis[i] = 1 , top --;
			else top ++ , tack[top] = i;
		}
	}
	int M = -1,f;
	for(int i = 1 ; i <= len ; i ++)
	{
		if(vis[i - 1] == 1 && vis[i] == 1)sum[i] = sum[i - 1];
		if(vis[i] == 1)sum[i] ++;
		if(sum[i] > M)M = sum[i] , f = i;
	}
	for(int i = f - sum[f] + 1 ; i <= f ; i ++)
	cout << s[i];
	return 0;
}

思路二:
考虑使用\(DP\)
设置状态:
\(dp[i]\)表示以\(字符i\)结尾的最长括号匹配的长度是多少。
假如\(字符i\)是'['或者'(',显然\(dp[i] == 0\)

否则:
如果\(s[i] 可以匹配 以s[i - 1]为结尾的最长合法序列的最左边的左边一个\)
\(dp[i]=dp[i-1]+2+dp[i-dp[i-1]-2];\)//+2是因为左右两个匹配可以使得最长合法序列长度+2
\((i - 1) - dp[i - 1]\) //这个便是以s[i-1]为结尾的最长合法序列最左边的左边一个

两个方法时空复杂度都是O\((n)\)

Get_top

Harbingers

题意简化:

抄袭自:队爷Boshi的博客

给定一棵树,除1号以外的每个节点上都有一个快递小哥。

每个节点都可能有快递要送到一号节点去,而快递也总是沿最短路运输。

快递每经过一个节点,可以选择:继续由当前的快递小哥运输,或更换这个节点的快递小哥运输。

每个快递小哥有2个参数,分别为 \(S_i\),\(T_i\) 表示接到快递出发前的准备时间,和通过单位距离的时间。

现在给出 \(n\)(\(n\)<=100000) 个节点和 \(n-1\) 条给定长度的路,求每个节点的快递运送到1号节点的最短时间。

思路:

首先,这个题目的暴力我想应该人人都会吧?

直接暴力设置状态:\(dp[i]\)表示\(i\)号点到\(1\)号点的最短时间。

转移方程即是:\(dp[i] = min(dp[j] + prepare[i] + (sum[j] - sum[i])*rate[i] )(j是i的祖先节点)\)

具体做法就是:

从根节点出发,每次往子节点扩散,然后子节点通过枚举祖先节点进行转移即可。

边界条件就是,\(dp[1] = 0\)(然鹅并没有什么用........)

直接到正解

Code

\(n^2\)的代码:
TLE成傻子

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

int n,cnt = 0;
int start[MAXN];
int sum[MAXN];
bool vis[MAXN];
int dp[MAXN];//i号节点运回1的最快速度
int prepare[MAXN], rate[MAXN] , fa[MAXN];

struct Edge {
	int next,to,w;
} edge[MAXN * 2];

void add(int from,int to,int w)
{
	edge[++cnt].next = start[from];
	edge[cnt].to = to;	
	start[from] = cnt;
	edge[cnt].w = w;
	return ;
}

void Solve(int x)
{
	int k = fa[x];
	dp[x] = prepare[x] + rate[x] * sum[x];//赋初值
	while(fa[k] != 0)
	{
		dp[x] = min(dp[x], dp[k] + prepare[x] + (sum[x] - sum[k]) * rate[x]);//暴力转移
		k = fa[k];
	}
	for(int i = start[x] ; i ; i = edge[i].next)
	{
		int to = edge[i].to;
		if(to == fa[x])continue;
		Solve(to);//暴力解决问题
	}
	return ;
}

void DFS(int x,int from)//计算树上前缀和以及预处理每个点的父节点
{
	vis[x] = 1;
	fa[x] = from;
	for(int i = start[x] ; i ; i = edge[i].next)
	{
		int to = edge[i].to;
		if(!vis[to])sum[to] = sum[x] + edge[i].w,DFS(to,x);//计算树上前缀和
	}
	return ;
}

int main()
{
	cin >> n;
	for(int i = 1 ; i <= n - 1 ; i ++)
	{
		int u , v ,w;
		cin >> u >> v >> w;
		add(u,v,w);
		add(v,u,w);
	}
	DFS(1,0);//预处理父节点
	for(int i = 2 ; i <= n ; i ++)
	{
		int x,y;
		cin >> x >> y;
		prepare[i] = x , rate[i] = y;
	}
	Solve(1);//开始暴力求解
	for(int i = 2 ; i <= n ; i ++)
	{
		cout << dp[i] << " ";//输出答案
	}
	return 0;
}

考虑拆柿子:

\(dp[i] = min(dp[j] + prepare[i] + (sum[j] - sum[i])*rate[i] )(j是i的祖先节点)\)
\(dp[i] = min(dp[j] + (sum[j] - sum[i])*rate[i] ) + prepare[i](j是i的祖先节点)\)
\(dp[i] = min(dp[j] + sum[j] * rate[i]) + prepare[i] - sum[i] * rate[i](j是i的祖先节点)\)

现在我们的柿子拆到一半了。

考虑对于\(i\),决策点\(k\)比决策点\(j\)要优,那么决策点\(k以及j\)满足什么关系?

不妨使用柿子,因为决策点\(k\)更优:

\(dp[k] + sum[k] * rate[i] < dp[j] + sum[j] * rate[i];\)

\(dp[k] - dp[j] < (sum[j] - sum[k]) * rate[i]\)

\(\frac{dp[k] - dp[j]}{sum[j] - sum[k]} < rate[i]\)

\(\frac{dp[k] - dp[j]}{sum[k] - sum[j]} > rate[i]\)

好哩,柿子变成了\(k\) = \(\frac{y1 - y2}{x1-x2}\)的,求斜率的样子。

把这玩意拍成二维点:\((sum[k],dp[k]),(sum[j],dp[j])\),这经过这两个点的直线斜率大于\(rate[i]\)即满足\(k\)要优于\(j\)

栈维护一个下凸包即可。时间复杂度O\((nlogn)\)

Get_Top

CF1092F Tree with Maximum Cost

Day 4. 11.11(双十一)

题意:

  • 给定一棵\(n\) 个节点的树,每个节点带有点权\(a_i\)
  • 定义\(dist(u,v)为u,v两个点的距离(每条边长度为1)\)
  • 请你选一个点\(u\),最大化:

\(\sum_{i = 1}^{i = n}{dis(u,i)*a_i}\)

现在问你最大化的\(\sum_{i = 1}^{i = n}{dis(u,i)*a_i}\)为多少。

\(1 <= n,a_i <= 2*10^5\)

\(input:\)

8
9 4 1 7 10 1 6 5
1 2
2 3
1 4
1 5
5 6
5 7
5 8

\(output:\)

121

样例解释:人工算出来应该是选择3号点得到答案的(确 信

思路:

思路一:

WTM直接暴力乱搞,O(\(n\))枚举所有点,跑一遍DFS计算每个点的距离,再暴力的去计算答案,时间复杂度O(\(n^2\))

思路二:

考虑贪心,但是感觉这个题目不是那么好贪心的样子........有哪个神想出了贪心的话,叫我一声(感觉貌似不能贪心?

思路三:

感觉思路一可以优化的样子,因为它的答案正确性貌似是毋庸置疑的,这比这个玄学贪心好多了,但是时间上貌似是不对的......

考虑如何优化。假设现在我们确定了一点\(u\)为选定的答案节点,获得的答案是\(Now\),假如我们把这个选定的点换成与它相邻的点\(p\)会怎么样?

显然,对于\(u\)为根的时候,子树\(p\)内的所有点,整个的贡献就会减少1,同时,原来以\(u\)为根的时候,不在子树\(p\)中的点的贡献都整体加了1。

于是,主角出来了:\(换根dp\)!

这玩意感觉就是暴力,为什么叫\(dp(大雾\),我们先确定以1为根的时候的答案(即取1为那一个用来计算答案的\(u\),同时预处理出每个“子树的大小”

  • 规定\(siz[v]\)为子树\(v\)的“子树的大小”

这里的“子树的大小”指的是每个子树中所有点的点权和,每次我们换一个根(注意,这里只能换成与当前根之间有边相连接的边!不然不好计算答案!)

然后每次我们将根移动到相邻的点,目前的\(Now\) = \(Now -siz[v] + (siz[1] - siz[v])\)因为\(siz[v]即是原来在子树v中的所有点的点权和,siz[1]-siz[v]就是不在子树v中的所有点的点权和\)

然后从1开始进行换根即可(不要反复横跳)

\((to != fa[x])\)

于是这东西就叫做"换根\(dp\)"了,又见识到了奇怪的\(dp\)

#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
const int MAXN = 2e5 + 500;
int n,cnt = 0;
int a[MAXN];
int start[MAXN];
int siz[MAXN],fa[MAXN],deep[MAXN],Now = 0,Ans = -1;

struct Edge {
	int next,to;
} edge[MAXN * 2];

inline int read()
{
	int x = 0, flag = 1;
	char ch = getchar();
	for( ; ch > '9' || ch < '0' ; ch = getchar());
	for( ; ch >= '0' && ch <= '9'; ch = getchar())x = (x << 3) + (x << 1) + ch - '0';
	return x * flag;
}

void add(int from,int to)
{
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].next = start[from];
	start[from] = cnt;
	return ;
}

int DFS1(int x,int from)
{
	siz[x] = a[x];
	fa[x] = from;//这个玩意我好像没有用到......
	deep[x] = deep[from] + 1;//记录一下深度,方便计算初始答案
	for(int i = start[x] ; i ; i = edge[i].next)
	{
		int to = edge[i].to;
		if(to != from)siz[x] += DFS1(to,x);//预处理整个子树的大小
	}
	return siz[x];
}

void Solve(int x,int from)
{
	int k = Now;
	if(x != 1)
	Now += (siz[1] - 2 * siz[x]);
	Ans = max(Ans,Now);//统计答案
	for(int i = start[x] ; i ; i = edge[i].next)
	{
		int to = edge[i].to;
		if(to == from)continue;
		Solve(to,x);//跟相邻的点换根
	}
	Now = k;
	return ;
}

inline void write(int x)
{
	if(x == 0){putchar('0');return;}
	char b[55];
	int R = 0;
	while(x)
		R ++ , b[R] = x % 10 + '0' , x /= 10;
	for(int i = R ; i >= 1 ; i --)
		putchar(b[i]);
}

signed main()
{
	n = read();
	for(int i = 1 ; i <= n; i ++)a[i] = read();
	for(int i = 1 ; i <= n - 1 ; i ++)
	{
		int u , v ;
		u = read() , v = read();
		add(u,v) , add(v,u);
	}
	DFS1(1,0);//先来一遍DFS进行预处理
	for(int i = 1 ; i <= n ; i ++)
		Now += (deep[i] - 1) * a[i];//先预处理出以1节点为答案节点的答案
	Solve(1,0);//开始换根
	write(Ans);
	// ------------------ end -------------------
	return 0;
}

(话说我时间复杂度也是O\((n)\)为什么这么卡,跑了32s),怕不是有上千个点.......

ps.
(后来发现原来是我用的cin的缘故,数据点比较多,然后每个数据都有亿点点大,改成快读后变成了2s)

Get_Top

蜈蚣

水话

这道题确实是套路的\(dp\),这里不仅仅讲一下这道题,顺便总结一下此类问题的套路

题意简化

将长度为\(n\)的序列分为\(m\)段,得到的每一段计算段内异或和,最后将异或和相加,要求最终得到的值最大。(题目背景太ex了)

做法:

先介绍\(dp\)三步法:

  • 划分子问题\(and\)设置状态。

  • 确定决策和状态转移方程。

  • 确定边界以及预处理减少时间复杂度。

设立状态:

很容易想到设立状态\(dp[i][j]\)表示前\(i\)个数字已经分成了\(j\)段得到的和。

为什么这么设立状态?

虽然这是套路,但是我们不妨思考一下这个问题。

首先,我们设立状态的目的是为了划分子问题并且方便转移以及决策。

这样子设立状态很显然的就可以划分子问题了。

因为本质上来说,对于你把整个序列分为\(m\)段和把前\(i\)个数组成的序列分为\(j\)段其实是同一个问题。而且将前\(i\)个数进行后相对于原问题比较容易解决,于是我们把子问题划分成了这个。

为什么这样设立状态会方便转移?

因为我们分的段,很显然是连续的,而我们设立的状态,恰好又是前\(i\)个划分\(j\)段,按照这样子的状态,我们很好去获得最优解,这样子划分就有利于转移。

状态转移方程

设立好了状态,那么就比较容易写出状态转移方程了:

\(dp[i][j] = max\)(\(dp[k][j -1]\) + \(sum[i]\) \(xor\) \(sum[k]\))(\(k < i\))

\(sum[i]\)表示的是将序列前\(i\)个数异或起来得到的结果。

\(sum[k]同理\)

\(sum[i]\) \(xor\) \(sum[k]\)得到的就是\(k + 1\)个数到第\(i\)个数的异或起来得到的结果了。

具体为什么,你不妨手玩几组样例或者画图。

边界的设立

\(dp[i][1] = sum[i]\)这个没什么好说的,很显然的事情。

最后的答案:

\(dp[n][m]\):根据我们划分的子问题可知

这一类问题的总结:

关于这种分组最值问题,显然是套路的\(dp\)

这种分组最值问题的做法:

状态的设置往往是\(dp[i][j]\)表示前\(i\)个数已经分成\(j\)组所获得的最大收益。

然后枚举也是相当的套路。

往往是三层\(for\)循环,第一层枚举前\(i\)个,第二层\(j\)枚举分\(j\)组,然后再枚举寻找最优决策即可。

分组最值问题的动态转移方程也是满满的套路:

\(dp[i][j] = max(or\) \(min\))(\(dp[k][j -1]\) + .....)(\(k < i\)),省略号即是一个转移需要计算的"贡献"。

这类问题几个坑点:

  • 最后的答案往往需要枚举一下整个序列分成多少组的时候得到最大值。
    (但是这个题就不要,因为强制要求分为\(m\)段)

  • 边界的设立大多数都是\(dp[i][1]\) = ... \(or\) \(dp[i][0]\) = ...,但是注意特殊情况!

  • 注意能用前缀和优化的东西尽量用前缀和优化

大概就是这些了。

Code:

#include <bits/stdc++.h>
using namespace std;
int n , m ;
int dp[1005][1205];
int sum[1005];
int main()
{
	cin >> n >> m;
	for(int i = 1 ; i <= n ; i ++)
	{
		int x;
		cin >> x;
		sum[i] = sum[i - 1] ^ x;//前缀和优化
		dp[i][1] = sum[i];//边界
	}
	for(int i = 1 ; i <= n ; i ++)
	{
		for(int j = 2 ; j <= m ; j ++)//起点不能从1开始了!注意!
		{
			for(int k = j - 1 ; k <= i - 1 ; k ++)//注意k的枚举起点是j-1
			dp[i][j] = max(dp[i][j],dp[k][j - 1] + (sum[i] ^ sum[k]));
		}
	}
	cout << dp[n][m];
	return 0;
}

Get_Top

[CF1443B] Saving the City

前言:

本人这篇题解按照规范的DP思路走。

所以会讲得比较模式化,希望能够理解。

做法:

采用线性的\(dp\)的方法,时间复杂度O(\(n\)),额外空间复杂度O(\(2*n\))

首先是一个小小的贪心。

去掉给出的串的前缀的0以及后缀的0,因为我们不需要处理这些0.

然后\(dp\)进行处理

  • 设立状态

\(dp[i][0]\)(这个状态是对于第\(i\)位字符为\(0\)的时候特有的状态)表示到第\(i\)位,不把第\(i\)位上的\(0\)变成\(1\)进行处理,将前面的串全部变为\(0\)所需要的最少费用。

\(dp[i][1]\)表示的是将当前位变为\(1\)进行处理(假如是\(1\)就不需要变),将前面的串变为\(0\)所需要的最小费用

  • 状态转移方程

分两种情况:

\(Case1\):当前字符为\(1\)

分情况讨论:

假如前一个字符为\(1\)

\(dp[i][1] = dp[i - 1][1];\)

假如前一个字符为\(0\)

\(dp[i][1] = min(dp[i - 1][0] + A,dp[i - 1][1])\)

(因为如果前一个是\(0\),不把它变为\(1\),那么我们不能承接前面的情况,只能把当前的\(1\)作为接下来连续的\(1\)组成的串的开头,费用加上A。不然就可以接上前面的)

\(Case2\)当前字符为\(0\)

\(dp[i][1] = dp[i - 1][1] + B\)

对于此种情况下对于\(dp[i][0]\)我们再结合当前字符的前一个字符进行分类讨论。

前一个字符为\(1\)\(dp[i][0] = dp[i - 1][1]\)

否则\(dp[i][0] = dp[i - 1][0]\)(前一个字符变为\(1\)但是这个字符不变为\(1\)显然没有意义。)

状态转移方程到此结束。

  • 边界的设立

\(dp[start][1] = A\)(这里是\(start\)是指第一个\(1\)出现的位置)

最后的答案会是\(dp[len][1]\)(这里的\(len\)是最后一个\(1\)出现的位置)

思路讲解到此结束。

Code

#include <bits/stdc++.h>
using namespace std;
int T;
int A,B;
int dp[100005][2];
char s[100005];
int main()
{
	cin >> T;
	for(int v = 1 ; v <= T ; v ++)
	{
		cin >> A >> B;
		cin >> s + 1;
		int len = strlen(s + 1);
		int start = 1;
		while(s[start] == '0')start ++;
		while(s[len] == '0')len --;
		dp[start][1] = A;
		for(int i = start + 1 ; i <= len ; i ++)
		{
			dp[i][1] = dp[i][0] = 0;
            //状态转移,前文已经介绍的很清楚了,不再赘述
			if(s[i] == '1')
			{
				if(s[i - 1] == '1')
				dp[i][1] = dp[i - 1][1];
				else dp[i][1] = min(dp[i - 1][1] , dp[i - 1][0] + A);
			}
			else 
			{
				dp[i][1] = dp[i - 1][1] + B;
				if(s[i - 1] == '1')
				dp[i][0] = dp[i - 1][1];
				else dp[i][0] = dp[i - 1][0];
			}
		}
		cout << dp[len][1] << endl;
	}
	return 0;
}

后话

这个做法一开始我没敢保证是对的,仔细想想感觉没有什么问题,然后Hack了自己几组数据,没有Hack成功,要是哪位仁兄Hack 掉了我,及时私信我,我会做出修改。

Get_Top

[USACO 10MAR]Great Cow Gathering G

又是一道换根\(dp\)

这道题和之前那一道换根\(DP\)没有很大区别,只是带了边权而已。

贴代码算了,有需要的话点这里看解析:CF1092F(Tree with Maximum Cost)

#include <bits/stdc++.h> 
using namespace std;
#define int long long 
const int MAXN = 100005;
int n,cnt = 0;
int C[MAXN],siz[MAXN];
int start[MAXN],sum[MAXN] = {0},Now = 0,Ans = 1000000000000000000;

struct Edge {
	int next,to,w;
} edge[MAXN * 2];

inline int read()
{
	int x = 0, flag = 1 ;
	char ch = getchar();
	for( ; ch > '9' || ch < '0' ; ch = getchar())if(ch == '-')flag = -1;
	for( ; ch >= '0' && ch <= '9' ; ch = getchar())x = (x << 3) +  (x << 1) + ch - '0';
	return x * flag;
}

void add(int from,int to,int w)
{
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].next = start[from];
	edge[cnt].w = w;
	start[from] = cnt;
	return ;
}

int DFS(int x,int from)
{
	siz[x] = C[x];
	for(int i = start[x] ; i ; i = edge[i].next)
	{
		int to = edge[i].to;
		if(to == from)continue;
		sum[to] = sum[x] + edge[i].w;
		siz[x] += DFS(to,x);
	}
	return siz[x];
}

void Get_Ans(int x,int from,int ans)
{
	Ans = min(Ans,ans);
	for(int i = start[x] ; i ; i = edge[i].next)
	{
		int to = edge[i].to, w = edge[i].w;
		if(to == from)continue;
		Get_Ans(to,x,ans + siz[1] * w - 2 * siz[to] * w);
	}
	return ;
}

signed main()
{
	n = read();
	for(int i = 1 ; i <= n ; i ++)C[i] = read();
	for(int i = 1 ; i <= n - 1 ; i ++)
	{
		int u = read(), v = read(), w = read();
		add(u,v,w);
		add(v,u,w);
	}
	DFS(1,0);
	for(int i = 1 ; i <= n ; i ++)Now += sum[i] * C[i];
	Get_Ans(1,0,Now);
	cout << Ans;
	return 0;
}

Get_Top

posted @ 2020-11-09 15:25  MYCui  阅读(395)  评论(0编辑  收藏  举报