文章涉及的题目都是codeforce里problemset里的题目,tag为dp的题目,建议大家只看思路,代码自己写。
1、Job Lookup
题目链接:题目链接
分析:
根据题目要求当前节点的左子树的所有节点的标号都要小于,右子树的所有节点的标号都要大于,也就是说对于区间变为和,以及,那么我们考虑用区间进行解决问题,我们把所作的贡献分布在每一条边里,当我们从当前节点转移到子树时,子树的值所作的贡献即不在子树中的节点和子树中的的总和,这个总和我们可以通过二维前缀和处理出来,所以总的时间复杂度为的。
代码:
#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
题目链接:题目链接
分析:
考虑相邻的两个点,我们发现他们没法两个点都为,仅当为是满足两个点都为,所以如果我们在遍历树的时候一定是取而所有不取,或不取则取或不取都行,显然这是典型的树形的形式,但是题目还要求所取的权值总和尽可能小,我们可以取数组为类型,进行转移和判断从而求解,当然我们也可以用一个额外的数组记录这个数,总之就是需要根据这个值多写一些判断从而进行状态转移。
代码:
#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;
}