最近遇到的各种DP 1.0
文章目录
前言
全是codeforces上的,1900~2200左右吧,可能会一直更新这个专题。
CF82D Two out of Three (巧妙的转移)
题目链接
http://codeforces.com/problemset/problem/82/D
题意:
(来自洛谷)
一队顾客排在一位收银员前面。他采取这样一个策略:每次,假如队伍有至少两人,就会从前面的前三人(如果有)中选取两位一起收银,所花费的时间为这两人单独收银所需时间的最大值。如果只有两人,那么一起收银;如果只有一人,那么单独收银。请问所需的总时间最少是多少? 1 ≤ n ≤ 1000 , 1 ≤ a i ≤ 1 0 6 1 \le n \le 1000 , 1 \le a_i \le 10^6 1≤n≤1000,1≤ai≤106
Input
4
1 2 3 4
Output
6
1 2
3 4
(输出最少总时间和收银方式)
思路:
比较巧妙的动态转移。
每次在前3个人里选2个,手推一下可以发现,第 i i i轮收银开始时,前 2 ∗ i − 1 2*i-1 2∗i−1号剩且仅剩一个人在队列中。
也就是说 , 第i轮收银的前三人编号一定为: j , 2 ∗ i , 2 ∗ i + 1 , j ϵ [ 1 , 2 ∗ i − 1 ] j , 2*i , 2*i+1 , j \epsilon [1,2*i-1] j,2∗i,2∗i+1,jϵ[1,2∗i−1]。
我们令 dp[i][j] 为 第 i 轮 第 j 个人的情况,可得转移方程:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ 2 ∗ i ] , t [ 2 ∗ i + 1 ] ) ) dp[i][j] = max( dp[i-1][j] , max(t[2*i],t[2*i+1]) ) dp[i][j]=max(dp[i−1][j],max(t[2∗i],t[2∗i+1]))
这里得分三种情况讨论,因为是在 j , i ∗ 2 , i ∗ 2 + 1 j , i*2 , i*2+1 j,i∗2,i∗2+1 这三个人中选2个。
设 a = 2i , b = 2i+1
1.选 a,b
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ a ] , t [ b ] ) ) dp[i][j] = max(dp[i-1][j],max(t[a],t[b])) dp[i][j]=max(dp[i−1][j],max(t[a],t[b]))
2.选 j,a
d p [ i ] [ b ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ j ] , t [ a ] ) ) dp[i][b] = max(dp[i-1][j],max(t[j],t[a])) dp[i][b]=max(dp[i−1][j],max(t[j],t[a]))
3.选 j,b
d p [ i ] [ a ] = m a x ( d p [ i − 1 ] [ j ] , m a x ( t [ j ] , t [ b ] ) ) dp[i][a] = max(dp[i-1][j],max(t[j],t[b])) dp[i][a]=max(dp[i−1][j],max(t[j],t[b]))
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int dp[N][N];
struct node{
int i;
int j;
int last;
};
node path[N][N];
int t[N];
int n;
int main(){
ios::sync_with_stdio(false);
cin>>n;
int m = n;
for(int i=1;i<=n;i++)
cin>>t[i];
t[++n] = 0; // 最后补一个权值为0的人可以避免奇数特判
memset(dp,0x3f3f3f3f,sizeof dp);
dp[1][1] = max(t[2],t[3]),path[1][1] = {2,3,3};
dp[1][2] = max(t[1],t[3]),path[1][2] = {1,3,3};
dp[1][3] = max(t[1],t[2]),path[1][3] = {1,2,3};
// 第1轮:1,2,3
// 第2轮:1,2,3,4,5
// 第i轮的候选人必为 : j , i*2 , i*2+1 (其中 1 <= j <= i*2-1 )
for(int i=2;i<=n/2;i++){
int a = i*2,b = i*2+1;
for(int j=1;j<2*i;j++){
if(dp[i-1][j]+max(t[a],t[b])<dp[i][j]){
dp[i][j] = dp[i-1][j]+max(t[a],t[b]);
path[i][j] = {a,b,j}; // path记录路径 : a,b是当前收银的两个人, j指从j继承过来
}
if(dp[i-1][j]+max(t[j],t[a])<dp[i][b]){
dp[i][b] = dp[i-1][j]+max(t[j],t[a]);
path[i][b] = {j,a,j};
}
if(dp[i-1][j]+max(t[j],t[b])<dp[i][a]){
dp[i][a] = dp[i-1][j]+max(t[j],t[b]);
path[i][a] = {j,b,j};
}
}
}
cout<<dp[n/2][n]<<endl;
// 最后一个收银的一定是dp[n/2][n] , 然后通过上面存好的路径path.last一个一个找回去
stack<pair<int,int>> st;
int now = n;
for(int i=n/2;i>=1;i--){
st.push(make_pair(path[i][now].i,path[i][now].j));
now = path[i][now].last;
}
while(!st.empty()){
if(st.top().second>m){
cout<<st.top().first<<endl;
}
else
cout<<st.top().first<<" "<<st.top().second<<endl;
st.pop();
}
}
CF533B Work Group(树形dp)
题目链接:
http://codeforces.com/problemset/problem/533/B
题意:
公司有n个人,1是总裁,每个人有一个直接上司。每一个人有一个权值 a i a_i ai,要求找一个集合,使集合中所有人权值之和最大。其中每一个人的下属(直接,间接)总数都必须是偶数。输出最大权值。 1 ≤ n ≤ 2 ∗ 1 0 5 , 1 ≤ a i ≤ 1 0 5 1 \le n \le 2*10^5 , 1 \le a_i \le 10^5 1≤n≤2∗105,1≤ai≤105
Input
7
-1 3
1 2
1 1
1 4
4 5
4 3
5 2
Output
17
(输入第一行是n , 输入第i+1行表示第i个人的直接上司是x,它的权值是y)
思路:
蛮简单的一道树形dp,我却做了好久 23333.
一开始没看到间接下属也算 23333.
d p [ N ] [ 2 ] dp[N][2] dp[N][2] , 表示目前是第i个人,它的子树是 奇数/偶数 情况下的合法最优解。
我一开始感觉要再开一维 d p [ N ] [ 2 ] [ 2 ] dp[N][2][2] dp[N][2][2] , 第3维表示该节点选或不选,但其实没必要,因为在合法情况下奇数必选偶数必不选。
dp[u][0] = max(dp[v][0]+x0,dp[v][1]+x1);
dp[u][1] = max(dp[v][1]+x0,x1+dp[v][0]);
其中 x 0 = d p [ u ] [ 0 ] , x 1 = d p [ u ] [ 1 ] x_0 = dp[u][0] ,x_1 = dp[u][1] x0=dp[u][0],x1=dp[u][1]
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5+100;
struct E{
int to;
int nxt;
}e[N<<1];
int head[N],tot;
int dp[N][2];
int a[N];
void add_edge(int u,int v){
e[++tot].nxt = head[u];
e[tot].to = v;
head[u] = tot;
}
int n;
void dfs(int u,int f){
dp[u][1] = -0x3f3f3f3f;
for(int i=head[u];i;i=e[i].nxt){
int v = e[i].to;
if(v==f) continue;
dfs(v,u);
int x0 = dp[u][0],x1 = dp[u][1];
dp[u][0] = max(dp[v][0]+x0,dp[v][1]+x1);
dp[u][1] = max(dp[v][1]+x0,x1+dp[v][0]);
}
dp[u][1] = max(dp[u][1],dp[u][0]+a[u]); // 这里主要是为了判叶子节点
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;
int rt = 0;
for(int i=1;i<=n;i++){
int x;
cin>>x>>a[i];
if(x==-1) rt = i;
else{
add_edge(i,x);
add_edge(x,i);
}
}
dfs(rt,0);
cout<<max(dp[rt][1],dp[rt][0])<<endl;
}
CF296B Yaroslav and Two Strings(计数)
题目链接
https://codeforces.com/problemset/problem/296/B
题意
如果两个只包含数字且长度为 n 的字符串 s 和 w 存在两个数字 1 ≤ i , j ≤ n 1\leq i,j\leq n 1≤i,j≤n,使得 s i < w i , s j > w j s_i<w_i,s_j>w_j si<wi,sj>wj,则称 s s s 和 w w w 是不可比的。现在给定两个包含数字和问号且长度为 n 的字符串,问有多少种方案使得将所有问号替换成0到9的数字后两个字符串是不可比的?
input
2
90
09
output
1
input
2
11
55
output
0
思路
考虑 d p [ N ] [ 4 ] dp[N][4] dp[N][4]:
0 : 前面不出现 s j > w j s_j>w_j sj>wj 的情况
1 : 前面不出现 s j < w j s_j<w_j sj<wj 的情况
2:前面既有 s j > w j s_j>w_j sj>wj ,也有 s j < w j s_j<w_j sj<wj 的情况
3:前面全是 s j = w j s_j = w_j sj=wj 的情况
那么,对于当前第 i 项来说,无论是否存在 ′ ? ′ '?' ′?′ , 都可以分为3种情况:
s i > w i s_i > w_i si>wi
s i = w i s_i = w_i si=wi
s i < w i s_i < w_i si<wi
此时每个 d p [ i ] [ ] dp[i][ ] dp[i][]可以从 d p [ i − 1 ] [ ] dp[i-1][ ] dp[i−1][]中继承过来,最后答案即为 d p [ n ] [ 2 ] dp[n][2] dp[n][2]。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+100;
const int mod = 1e9+7;
int dp[N][4];
char s[N],w[N];
signed main(){
ios::sync_with_stdio(false);
int n;
cin>>n;
cin>>(s+1);
cin>>(w+1);
dp[0][3] = 1;
for(int i=1;i<=n;i++){
if(s[i]!='?'&&w[i]!='?'){
if(s[i]>w[i]){
dp[i][0] = 0;
dp[i][1] = (dp[i-1][1]+dp[i-1][3])%mod;
dp[i][2] = (dp[i-1][0]+dp[i-1][2])%mod;
dp[i][3] = 0;
}
else if(s[i]<w[i]){
dp[i][0] = (dp[i-1][0]+dp[i-1][3])%mod;
dp[i][1] = 0;
dp[i][2] = (dp[i-1][2]+dp[i-1][1])%mod;
dp[i][3] = 0;
}
else{
dp[i][0] = dp[i-1][0];
dp[i][1] = dp[i-1][1];
dp[i][2] = dp[i-1][2];
dp[i][3] = dp[i-1][3];
}
}
else if (s[i]=='?'&&w[i]!='?'){
int big = '9'-w[i]; // big: s>w的情况数, small: s<w的情况数, equal: s=w情况数
int small = w[i]-'0';
int equal = 1;
dp[i][0] = (small*dp[i-1][3]%mod+(small+equal)*dp[i-1][0]%mod)%mod;
dp[i][1] = (big*dp[i-1][3]%mod+(big+equal)*dp[i-1][1]%mod)%mod;
dp[i][2] = (10*dp[i-1][2]%mod + small*dp[i-1][1]%mod + big*dp[i-1][0]%mod)%mod;
dp[i][3] = equal*dp[i-1][3]%mod;
}
else if(s[i]!='?'&&w[i]=='?'){
int big = s[i]-'0'; // big: s>w的情况数, small: s<w的情况数, equal: s=w情况数
int small = '9'-s[i];
int equal = 1;
dp[i][0] = (small*dp[i-1][3]%mod+(small+equal)*dp[i-1][0]%mod)%mod;
dp[i][1] = (big*dp[i-1][3]%mod+(big+equal)*dp[i-1][1]%mod)%mod;
dp[i][2] = (10*dp[i-1][2]%mod + small*dp[i-1][1]%mod + big*dp[i-1][0]%mod)%mod;
dp[i][3] = equal*dp[i-1][3]%mod;
}
else{
int big = 45,small = 45,equal = 10; //显然,当s[i],w[i]都是“?”,一共100种取法,s>w的45种 s=w的10种
dp[i][0] = (small*dp[i-1][3]%mod+(small+equal)*dp[i-1][0]%mod)%mod;
dp[i][1] = (big*dp[i-1][3]%mod+(big+equal)*dp[i-1][1]%mod)%mod;
dp[i][2] = (100*dp[i-1][2]%mod + small*dp[i-1][1]%mod + big*dp[i-1][0]%mod)%mod;
dp[i][3] = equal*dp[i-1][3]%mod;
}
}
cout<<dp[n][2]%mod<<endl;
}
CF1201D Treasure Hunting(乱搞)
链接
https://codeforces.com/problemset/problem/1201/D
题意
洛谷的中文题面:
input
3 3 3 2
1 1
2 1
3 1
2 3
output
6
input
3 5 3 2
1 2
2 3
3 1
1 5
output
8
样例解释
思路
其实思路很简单但是被我写的特别冗杂。
可以把题目理解成一栋楼里有好几条只能向上走的电梯(下面都称作电梯),要求走最少的步数拿到楼里所有的宝藏。
可以发现事实上对于任意一层楼,只有最左的宝藏相邻的两个电梯和最右的宝藏相邻的两个电梯这4个电梯可能被选择。
那么我们只需要一个 d p [ N ] [ 4 ] dp[N][4] dp[N][4] , 让第 i 层和第 i-1 层进行一个4*4的暴力转移就行了。
实现上,我们对每层楼的宝藏位置排个序,找到第一个和最后一个宝藏,然后二分查找相邻的两个电梯。
(其实用lower_bound(),upper_bound()就行了)
这样的复杂度是 O ( 16 ∗ n ∗ l o g n ) O(16*n*logn) O(16∗n∗logn)的。
代码
(怎么我一写代码就写得好乱啊,呜呜呜)
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N= 2e5+1000;
int dp[N][4];
vector<int> v[N];
int n,m,k,q;
int b[N];
const int inf = 1e15;
// cal() 计算在i层楼时,我从from位置开始,到to位置结束,吃完该楼所有宝藏的最短路径(这里写的特别冗杂)
// 事实上就是在一个数轴上从from开始,到to结束,必须经过begin,end这两点的最短路径
int cal(int row,int from,int to){
int begin = v[row][0],end = v[row][v[row].size()-1]; // 第一个和最后一个宝藏
if(from>to) swap(from,to);
if(from==-inf||to==inf) return inf;
if(begin==end&&from==to) return 2*abs(begin-from);
if(begin==end){
if(from<=begin&&begin<=to)
return abs(from-to);
else if(begin<=from)
return to - begin + from - begin;
else
return begin-from + begin - to;
}
if(from<=begin&&end<=to){
return to-from;
}
if(from>=begin&&to<=end){
return (to-from)+2*abs(from-begin)+2*abs(end-to);
}
if(from>=begin){
return (to-from)+2*abs(from-begin);
}
if(to<=end){
return (to-from)+2*abs(end-to);
}
}
// 二分查找第一个位置小于等于now的电梯
int small(int now){
int l = 0,r = q+1;
int ans = l;
while(l<=r){
int mid = (l+r)>>1;
if(b[mid]>now){
r = mid-1;
}
else{
ans = mid;
l = mid+1;
}
}
return ans;
}
// 二分查找第一个位置大于等于now的电梯
int big(int now){
int l = 0,r = q+1;
int ans = r;
while(l<=r){
int mid = (l+r)>>1;
if(b[mid]>now){
ans = mid;
r = mid-1;
}
else{
l = mid+1;
}
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin>>n>>m>>k>>q;
v[1].push_back(1);
for(int i=1;i<=k;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y); // 第x层楼有个宝藏y
}
for(int i=0;i<=n;i++){
for(int j=0;j<4;j++)
dp[i][j] = inf;
}
int mx = n;
for(int i=n;i>=1;i--){
if(!v[i].empty()) break;
mx--;
}
for(int i=1;i<=q;i++) cin>>b[i];
b[0] = -inf;b[q+1] = inf; // 加两个电梯在正负无穷,避免二分时出锅
sort(b,b+q+2); // 给电梯从左到右排个序
for(int i=1;i<=n;i++){
if(v[i].empty()) continue;
sort(v[i].begin(),v[i].end()); // 给每层楼的宝藏排序
}
int from[4] = {1,1,1,1}; // from[] 记录上一层楼使用的4个电梯位置
dp[0][0] = dp[0][1] = dp[0][2] = dp[0][3] = 0;
for(int i=1;i<=n;i++){
if(v[i].empty()){
for(int j=0;j<4;j++) dp[i][j] = dp[i-1][j]; // 如果当前楼层没有宝藏,那显然我们直接坐电梯上去就好
continue;
}
int sa = small(v[i][0]); // 最左宝藏左边的电梯号
int sb = big(v[i][0]); // 最左宝藏右边的电梯号
int sc = small(v[i][v[i].size()-1]); // 最右宝藏左边的电梯号
int sd = big(v[i][v[i].size()-1]); // 最右宝藏左边的电梯号
int to[4] = {b[sa],b[sb],b[sc],b[sd]}; // to [] 这层楼使用的4个电梯位置
// 这里要非常注意,到达顶层以后,到最后一个宝藏就可以停下来了,所以顶层的to[] 不再是电梯位置,而是左右两个宝藏的位置
if(i==mx) to[0] = v[i][0],to[1] = v[i][v[i].size()-1];
for(int j=0;j<4;j++){
for(int z=0;z<4;z++){
// cal() 计算在i层楼时,我从from[z]位置开始,到to[j]位置结束,吃完该楼所有宝藏的最短路径
int ans = cal(i,from[z],to[j]);
dp[i][j] = min(dp[i][j],dp[i-1][z]+ans);
}
}
for(int j=0;j<4;j++) from[j] = to[j]; // 把 from 更新成 to
}
int ans = inf;
for(int i=0;i<4;i++) ans = min(ans,dp[n][i]);
cout<<ans+(mx-1)<<endl; // 别忘了向上走也算1步
return 0;
}