CF刷题记录:动态规划(4)

文章涉及的题目都是codeforce里problemset里的题目,tag为dp的题目,建议大家只看思路,代码自己写。

1、Job Lookup

题目链接:题目链接

分析:

根据题目要求当前节点x的左子树的所有节点的标号都要小于x,右子树的所有节点的标号都要大于x,也就是说对于区间[1,n]变为[1,x-1]x,以及[x+1,n],那么我们考虑用区间dp进行解决问题,我们把c_{uv}所作的贡献分布在每一条边里,当我们从当前节点转移到子树时,子树c的值所作的贡献即不在子树中的节点v和子树中的uc_{uv}总和,这个总和我们可以通过二维前缀和处理出来,所以总的时间复杂度为n^{3}的。

代码:

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

typedef long long ll;

const int N = 210;

int n;
int c[N][N], p[N];
ll sc[N][N], dp[N][N];

ll get(int l1, int r1, int l2, int r2)
{
	return sc[l2][r2] - sc[l2][r1 - 1] - sc[l1 - 1][r2] + sc[l1 - 1][r1 - 1];
}

void dfs(int l, int r, int la)
{
	if(l == r)
	{
		p[l] = la;
		return ;
	}

	int pos;
	for(int k = l; k <= r; k ++ )
	{
		ll sum = 0;
		if(k - 1 >= l)
			sum += dp[l][k - 1] + get(l, 1, k - 1, l - 1) + get(l, k, k - 1, n);
		if(r >= k + 1)
			sum += dp[k + 1][r] + get(k + 1, 1, r, k) + get(k + 1, r + 1, r, n);
		if(dp[l][r] == sum)
		{
			pos = k;
			break;
		}
	}

	p[pos] = la;
	if(pos - 1 >= l) dfs(l, pos - 1, pos);
	if(r >= pos + 1) dfs(pos + 1, r, pos);
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ )
		for(int j = 1; j <= n; j ++ )
			scanf("%d", &c[i][j]);

	for(int i = 1; i <= n; i ++ )
		for(int j = 1; j <= n; j ++ )
			sc[i][j] = sc[i - 1][j] + sc[i][j - 1] - sc[i - 1][j - 1] + c[i][j];

	memset(dp, 0x3f, sizeof dp);
	for(int i = 1; i <= n; i ++ ) dp[i][i] = 0;
	for(int len = 2; len <= n; len ++ )
		for(int i = 1; i + len - 1 <= n; i ++ )
		{
			int j = i + len - 1;
			for(int k = i; k <= j; k ++ )
			{
				ll sum = 0;
				if(k - 1 >= i)
					sum += dp[i][k - 1] + get(i, 1, k - 1, i - 1) + get(i, k, k - 1, n);
				if(j >= k + 1)
					sum += dp[k + 1][j] + get(k + 1, 1, j, k) + get(k + 1, j + 1, j, n);
				dp[i][j] = min(dp[i][j], sum);
			}
		}

	dfs(1, n, 0);

	for(int i = 1; i <= n; i ++ ) printf("%d ", p[i]);
	puts("");

	return 0;
}

2、1646D

题目链接:题目链接

分析:

考虑相邻的两个点,我们发现他们没法两个点都为good,仅当n2是满足两个点都为good,所以如果我们在遍历树的时候一定是ugood而所有v不取,或u不取则v取或不取都行,显然这是典型的树形dp的形式,但是题目还要求所取的权值总和尽可能小,我们可以取dp数组为pair类型,进行转移和判断从而求解,当然我们也可以用一个额外的数组记录这个数,总之就是需要根据这个值多写一些判断从而进行状态转移。

代码:

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

typedef pair<int, int> pii;

const int N = 200010;

int n;
int in[N], w[N];
int h[N], e[N * 2], ne[N * 2], idx;
pii dp[N][2];

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u, int la)
{
	pii t1 = make_pair(0, 0), t2 = make_pair(0, 0);
	dp[u][1] = {1, in[u]}, dp[u][0] = {0, 1};
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int v = e[i];
		if(v == la) continue;

		dfs(v, u);
		int cur = 0;
		t1.first += dp[v][0].first, t1.second += dp[v][0].second;
		if(dp[v][0].first != dp[v][1].first) 
		{
			if(dp[v][0].first > dp[v][1].first) cur = 0;
			else cur = 1;
		}
		else
		{
			if(dp[v][0].second < dp[v][1].second) cur = 0;
			else cur = 1;
		}
		t2.first += dp[v][cur].first, t2.second += dp[v][cur].second;
	}

	dp[u][1].first += t1.first, dp[u][1].second += t1.second;
	dp[u][0].first += t2.first, dp[u][0].second += t2.second;
}

void get(int u, int t, int la)
{
	if(t == 1) w[u] = in[u];
	else w[u] = 1;
	for(int i = h[u]; i != -1; i = ne[i])
	{
		int v = e[i];
		if(v == la) continue;

		if(t == 1) get(v, 0, u);
		else
		{
			int cur = 0;
			if(dp[v][0].first != dp[v][1].first) 
			{
				if(dp[v][0].first > dp[v][1].first) cur = 0;
				else cur = 1;
			}
			else
			{
				if(dp[v][0].second < dp[v][1].second) cur = 0;
				else cur = 1;
			}
			get(v, cur, u);
		}
	}
}

void take(int u, int t)
{
	get(u, t, 0);
	for(int i = 1; i <= n; i ++ ) printf("%d ", w[i]);
	puts("");
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ ) h[i] = -1;
	for(int i = 1; i <= n - 1; i ++ )
	{
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
		in[a] ++ , in[b] ++ ;
	}

	if(n == 2)
	{
		printf("2 2\n");
		printf("1 1\n");
		return 0;
	}

	dfs(1, 0);

	if(dp[1][0].first != dp[1][1].first)
	{
		if(dp[1][0].first > dp[1][1].first)
		{
			printf("%d %d\n", dp[1][0].first, dp[1][0].second);
			take(1, 0);
		}
		else
		{
			printf("%d %d\n", dp[1][1].first, dp[1][1].second);
			take(1, 1);
		}
	}
	else
	{
		if(dp[1][0].second < dp[1][1].second)
		{
			printf("%d %d\n", dp[1][0].first, dp[1][0].second);
			take(1, 0);
		}
		else
		{
			printf("%d %d\n", dp[1][1].first, dp[1][1].second);
			take(1, 1);
		}
	}

	return 0;
}

3、1616D

题目链接:题目链接

分析:

对于题目要求,我们首先关注一段序列如何能够全部取完,首先我们考虑如果任意长度为2都满足条件能否退出满足这样的一段整体都满足条件,显然我们还需要考虑其长为3时也满足才行,可以证明当一段长度大于3的段如果其中任意元素i其向前长2和长3都满足条件,则整体线段都满足,因为当长4时可以分成长为2,2,类似处理可以推出其他长度也满足,这里我在做题目时想的是处理出每个元素i向前最多能到达的地方,但是我们这样仍不很确定dp状态怎么转移,其实这里有个dp很常见的转移技巧,就是对于长度不定的一段,我们可以将其分成相邻的两个,因为我们在dp过程中把所有可能都枚举过了,同样也枚举过整个段都取或任意取的情况了,所以我们将其变成分析i和i-1即可O(n)的进行状态转移,我们判断i取或不取,但是因为条件我们还需要判断i-1取或不取,并且因为长2时3不一定满足,只有长3也满足时我们才能扩展到任意长度,所以我们还需要判断i-1的前面一个取或不取,所以我们定义状态为dp[i][0/1][0/1],这里我判断的时候有一个问题,我们判断长2可以直接取a[i]+a[i-1],长为3时,我下意识认为i-2和i-1满足2,i-1和i满足2,那么我们判断i-2和i满足2时一定满足i-2,i-1,i的3,但是这样满足的集合是3满足的子集,也就是说我们把范围变小了,计数减少了,直接判断a[i-2]+a[i-1]+a[i]>=x*3即可,并且a[i-2]+a[i-1]+a[i]>=x*3满足时a[i-2]+a[i]>=x*2不一定满足,这个问题找了我好久。

代码:

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

const int N = 50010;

int n, x;
int a[N];
int dp[N][2][2];

void solve()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	cin >> x;
	for(int i = 1; i <= n; i ++ ) a[i] -= x;

	for(int i = 0; i <= n; i ++ )
		for(int j = 0; j < 2; j ++ )
			for(int k = 0; k < 2; k ++ )
				dp[i][j][k] = 0;

	dp[1][1][0] = 1;
	for(int i = 2; i <= n; i ++ )
	{
		dp[i][0][0] = max(dp[i - 1][0][0], dp[i - 1][0][1]);
		dp[i][0][1] = max(dp[i - 1][1][0], dp[i - 1][1][1]);
		dp[i][1][0] = max(dp[i - 1][0][0], dp[i - 1][0][1]) + 1;
		if(i - 1 >= 1 && a[i - 1] + a[i] >= 0) dp[i][1][1] = max(dp[i][1][1], dp[i - 1][1][0] + 1);
		if(i - 2 >= 1 && a[i - 1] + a[i] >= 0 && a[i - 2] + a[i - 1] + a[i] >= 0) dp[i][1][1] = max(dp[i][1][1], dp[i - 1][1][1] + 1);
	}

	int ans = 0;
	for(int i = 1; i <= n; i ++ )
		for(int j = 0; j < 2; j ++ )
			for(int k = 0; k < 2; k ++ )
				ans = max(ans, dp[i][j][k]);

	printf("%d\n", ans);
}

int main()
{
	int T; cin >> T; while(T -- ) solve();
	return 0;
}

4、1614D1

题目链接:题目链接

分析:

这道题目我是没有任何思路的,我无法想到怎么枚举出gcd进行计算,题解告诉我们我们可以定义dp[i]表示此时gcd为i时的最大价值,i一定为i的倍数j转移过来,并且我们记录元素为i的倍数的数量cnt[i]和cnt[j],则转移为dp[i]=max(dp[i],dp[j]+i*(cnt[i]-cnt[j]),也就是说当a[i]的最大值较小时,我们直接枚举值i从1到maxa[i],并且通过枚举倍数,显然这是可以在nlogn实现的,而关键就在于我们的每个元素可以转化为i的倍数的数量cnt[i],进而进行计算贡献。
代码:

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

typedef long long ll;

const int N = 5e6 + 10;

int n;
int a[N], cnt[N];
ll dp[N];

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	for(int i = 1; i <= n; i ++ )
		for(int j = 1; j <= a[i] / j; j ++ )
			if(a[i] % j == 0)
			{
				cnt[j] ++ ;
				if(j != a[i] / j) cnt[a[i] / j] ++ ;
			}

	for(int i = N - 1; i >= 1; i -- )
	{
		dp[i] = max(dp[i], 1ll * i * cnt[i]);
		for(int j = i; j < N; j += i)
			dp[i] = max(dp[i], dp[j] + 1ll * i * (cnt[i] - cnt[j]));
	}

	printf("%lld\n", dp[1]);
	return 0;
}

5、1614D2

题目链接:题目链接

分析:

其实我们上一道题目的正确思路过程没讲明为什么从后取gcd的值计算价值就是最优的,所以这里存在一步贪心,就是当我们取gcd为x时,gcd再变为x的因子y时,我们可以选择y到x的其他大于y的因子的尽可能的少,也就是说如果x到y中存在大于y的因子,我们需要先考虑转移到这个因子的计算,而上一题是将所有因子都枚举了一遍所以可行,那么在这题我们可以只枚举i的质数倍的数j,从而将转移过程变为nloglogn。
代码:

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

typedef long long ll;

const int N = 2e7 + 10;

int n;
int primes[N], idx;
int a[N], cnt[N];
bool vis[N];
ll dp[N];

void init()
{
	for(int i = 2; i < N; i ++ )
	{
		if(!vis[i]) primes[idx ++ ] = i;
		for(int j = 0; primes[j] * i < N; j ++ )
		{
			vis[primes[j] * i] = true;
			if(i % primes[j] == 0) break;
		}
	}
}

int main()
{
	init();
	cin >> n;
	for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	for(int i = 1; i <= n; i ++ ) cnt[a[i]] ++ ;
	for(int i = 1; i < N; i ++ )
		for(int j = i + i; j < N; j += i)
			cnt[i] += cnt[j];

	for(int i = 1; i < N; i ++ )
	{
		dp[i] = max(dp[i], 1ll * i * cnt[i]);
		for(int j = 0; primes[j] * i < N; j ++ )
			dp[primes[j] * i] = max(dp[primes[j] * i], dp[i] + (primes[j] * i - i) * 1ll * cnt[primes[j] * i]);
	}

	printf("%lld\n", *max_element(dp + 1, dp + N));
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wlzsgl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值