20230719-动态规划DP

20230719

线性DP

P1020 [NOIP1999 普及组] 导弹拦截

题目描述

传送门
给定一个长度为\(N\)的序列\(a\),求最少能划分成多少个不上升序列
\(1 \le N \le 10^5\)

Solution

一个小结论:
最小不上升子序列个数=最大上升子序列

很容易证明
每一个最大上升子序列中的数都是一个不上升子序列的开头

那这道题就是求最长上升子序列
我直接维护了一个树状数组……

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

const int maxn=1e5+10;
int ans,res,len=0,n,f[maxn],x,t[maxn],b[maxn],a[maxn];

int lowbit(int i){return i&(-i);}
void update(int x,int val){for(int i=x;i<=n;i+=lowbit(i)) t[i]=max(t[i],val);}
int query(int x){
  int res=0;
  for(int i=x;i>0;i-=lowbit(i)) res=max(res,t[i]);
  return res;
}

int main(){
  /*2023.7.20 H_W_Y P1020 [NOIP1999 普及组] 导弹拦截 DP*/
  while(scanf("%d",&x)!=EOF) a[++n]=x,b[n]=a[n];
  sort(b+1,b+n+1);
  len=unique(b+1,b+n+1)-b-1;
  for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+len+1,a[i])-b;
  for(int i=1;i<=n;i++){
  	f[i]=query(a[i]-1)+1;
  	ans=max(f[i],ans);
  	update(a[i],f[i]);
  	a[i]=n-a[i]+1;
  }
  memset(t,0,sizeof(t));
  for(int i=1;i<=n;i++){
  	f[i]=query(a[i])+1;
  	res=max(res,f[i]);
  	update(a[i],f[i]);
  }
  printf("%d\n%d\n",res,ans);
  return 0;
}

P2896 [USACO08FEB] Eating Together S

题目描述

传送门
给定一个长度为\(N\)的序列\(a\)
求最少改变多少数使其成为不上升或不下降序列
\(1 \le N \le 3 ∗ 10^5, 1 \le ai \le 3\)

Solution

就直接求出最长不上升或不下降子序列
在用\(n\)去减取min即可

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

const int maxn=3e5+10;
int n,a[maxn],lst[4],f[maxn],ans=0x3f3f3f3f;

int main(){
  /*2023.7.20 H_W_Y P2896 [USACO08FEB] Eating Together S DP*/ 
  scanf("%d",&n);
  for(int i=1;i<=n;i++){
  	scanf("%d",&a[i]);
  	for(int j=1;j<=a[i];j++) f[i]=max(f[i],f[lst[j]]+1);
  	lst[a[i]]=i;
  	ans=min(ans,n-f[i]);
  }
  lst[1]=lst[2]=lst[3]=0;
  for(int i=1;i<=n;i++){
  	f[i]=0;
  	for(int j=3;j>=a[i];j--) f[i]=max(f[i],f[lst[j]]+1);
  	lst[a[i]]=i;ans=min(ans,n-f[i]);
  }
  printf("%d\n",ans);
  return 0;
} 

P5017 [NOIP2018 普及组] 摆渡车

题目描述

传送门

\(n\)个人,分别在\(t_i\)的时候到车站,
只有一辆车$m$的时间可以往返一次
问如何安排车出发的时间,使等车时间之和最小
\(1 \le N \le 500, 1 \le t_i \le 4 ∗ 10^6, 1 \le m \le 100\)

Solution

很容易想到是dp
但是每一趟车发之前不一定是一个人到的时间

我先考虑了一个dp
\(dp[i]\)表示第\(i\)时刻发车的最小等待时间
而上一次发车是在\(i-2* m \sim i-m\)之间的
我们在用一个数组维护从第\(j\)时刻起到现在等着一辆车的时间
就有了\(O(mt)\)的做法
但是会TLE,只有70分

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

const int maxn=4e6+10,inf=0x3f3f3f3f;
int n,m,t[1005],f[maxn],dp[maxn],num[maxn],ans=inf;

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

int main(){
  /*2023.7.21 H_W_Y P5017 [NOIP2018 普及组] 摆渡车 DP*/ 
  n=read();m=read();
  for(int i=1;i<=n;i++) t[i]=read(),num[++t[i]]++;;
  sort(t+1,t+n+1);
  for(int i=1;i<=t[n]+m;i++) num[i]+=num[i-1],dp[i]=inf;
  for(int i=1;i<=t[n]+m;i++){
    for(int j=i-2*m;j<i;j++) f[j]+=num[i-1]-num[j];
    for(int j=i-2*m;j<=i-m;j++) dp[i]=min(dp[i],dp[j]+f[j]);
    if(i>=t[n]) ans=min(ans,dp[i]);   
  }
  printf("%d\n",ans);
  return 0;
}

看来是肯定不能枚举\(t\)
然而发现有很多\(t\)也都是没有用的
我们就考虑枚举每一个人的等车时间
很明显,最多会等\(2* m\)分钟
而我们可以在排序后从\(i\)推到\(i+1\)
\(f[i][j]\)表示第\(i\)个人等待\(j\)分钟的最小等待时间
这样我们就可以从\(f[i][j]\)转移到\(f[i+1][k]\)
此时\(k\)满足\(t[i]+j=t[i+1]+k\)或者\(t[i]+j+m \le t[i+1]+k\)
时间复杂度为\(O(nm^2)\)

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

const int inf=0x3f3f3f3f;
int n,m,t[1005],dp[1005][1005],ans=inf;

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

int main(){
  /*2023.7.21 H_W_Y P5017 [NOIP2018 普及组] 摆渡车 DP*/ 
  n=read();m=read();
  for(int i=1;i<=n;i++) t[i]=read();
  sort(t+1,t+n+1);
  for(int i=1;i<=n;i++)
    for(int j=0;j<=2*m;j++)
      dp[i][j]=inf;
  for(int i=0;i<n;i++)
    for(int j=0;j<=2*m;j++)
      if(dp[i][j]!=inf)
        for(int k=0;k<=2*m;k++)
          if(t[i]+j==t[i+1]+k||t[i]+j+m<=t[i+1]+k)
            dp[i+1][k]=min(dp[i+1][k],dp[i][j]+k);
  for(int i=0;i<=2*m;i++) ans=min(ans,dp[n][i]);
  printf("%d\n",ans);
  return 0;
}

P1025 [NOIP2001 提高组] 数的划分

题目描述

传送门
求将\(n\)分成\(k\)段不可以为\(0\)的方案数,考虑旋转重构,
\(1 \le n \le 200\)

Solution

数据范围小得可怜
乱搞一下dp就可以了

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

int n,k,f[7][205][205],ans=0;

int main(){
  /*2023.7.21 H_W_Y P1025 [NOIP2001 提高组] 数的划分 DP*/
  scanf("%d%d",&n,&k);
  for(int j=1;j<=n;j++) f[1][j][n-j]=1;
  for(int i=2;i<=k;i++)
    for(int j=1;j<=n;j++)
	  for(int k=j;k<=n;k++)
	    for(int p=j;p<=n;p++)
	      if(k>=p) f[i][p][k-p]+=f[i-1][j][k];
  for(int i=1;i<=n;i++) ans+=f[k][i][0];
  printf("%d\n",ans);
  return 0;
}

P5662 [CSP-J2019] 纪念品-背包

题目描述

传送门
\(n\) 类物品,现在手上有 \(m\) 元,已知未来 \(t\) 天,物品的价格
求最后最多能剩多少钱
\(n,t \le 100, m \le 1000\) 保证手上最多有 \(10^4\)

Solution

对于每一天,我们都做一个背包
其中的 \(val[i]\) 为今明两日的价格之差
如果我们明天卖了会赚
那我们明天一定会卖掉
这样做 \(t\) 次背包即可

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

int t,n,p[105][105],ans=0,tmp=0,dp[105][10005],c[105];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
  return x*f;
}

int main(){
  /*2022.10.26 H_W_Y P5662 [CSP-J2019] 纪念品 背包*/
  t=read();n=read();ans=read();
  for(int i=1;i<=t;i++)
    for(int j=1;j<=n;j++)
      p[i][j]=read();
  for(int i=1;i<t;i++){
  	tmp=0;
    for(int j=1;j<=n;j++){
      for(int k=0;k<=ans;k++) dp[j][k]=0;	
      c[j]=p[i+1][j]-p[i][j];
	}
    for(int j=1;j<=n;j++)
      for(int k=0;k<=ans;k++){
      	if(k>=p[i][j]&&c[j]>0) dp[j][k]=max(dp[j][k-p[i][j]]+c[j],dp[j-1][k]);
      	else dp[j][k]=dp[j-1][k];
	  }
    for(int j=0;j<=ans;j++) tmp=max(tmp,dp[n][j]);
    ans=ans+tmp;
  }
  printf("%d\n",ans);
  return 0;
}

P1280 尼克的任务

题目描述

传送门

Solution

很容易想到是用dp

我们考虑设 \(dp[i]\) 表示到第 \(i\) 分钟时最大的休息时间
那么当 \(i\) 时刻没有任务时
\(dp[i]=dp[i+1]+1\)
反之 \(dp[i]=\max{dp[i+a[j].lst]}\)
其中 \(a[j]\)是这一时刻开始的任务
这样就做完了

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

const int maxn=1e4+10;
int n,m,dp[maxn],it=1;
struct node{
  int st,lst;
  bool operator <(const node &rhs)const{
    if(st!=rhs.st) return st<rhs.st;
    return lst<rhs.lst;
  }
}a[maxn];

int main(){
  /*2023.7.22 H_W_Y P1280 尼克的任务 线性DP*/ 
  scanf("%d%d",&n,&m);
  for(int i=1;i<=m;i++) scanf("%d%d",&a[i].st,&a[i].lst);
  sort(a+1,a+m+1);it=m;
  for(int i=n;i>=1;i--){
  	if(a[it].st<i) dp[i]=dp[i+1]+1;
  	else{
  	  while(a[it].st==i){
  	    dp[i]=max(dp[i],dp[i+a[it].lst]);
		it--;	
	  }	
	}
  }
  printf("%d\n",dp[1]);
  return 0;
}

我一开始是考虑用 \(dp[i]\) 表示到第 \(i\) 个任务最大的休息时间
这样可以得到一个很明显的 dp 方程
然后就有了下面这份代码

H_W_Y-Coding-50分
#include <bits/stdc++.h>
using namespace std;

const int maxn=1e4+10;
int n,m,dp[maxn],mx;
struct node{
  int st,ed;
  bool operator <(const node &rhs)const{
    if(st!=rhs.st) return st<rhs.st;
    return ed<rhs.ed;
  }
}a[maxn]; 

int ub(int x){
  int l=1,r=m+1;
  while(l<r){
  	int mid=(l+r)/2;
  	if(a[mid].st<=x) l=mid+1;
  	else r=mid;
  }
  return l;
}

int main(){
  /*2023.7.22 H_W_Y P1280 尼克的任务 线性DP*/ 
  scanf("%d%d",&n,&m);
  for(int i=1;i<=m;i++){
  	scanf("%d%d",&a[i].st,&a[i].ed);
  	a[i].ed=a[i].st+a[i].ed-1;
  }
  sort(a+1,a+m+1);a[m+1].st=a[m].ed+2;
  mx=a[1].ed;dp[1]=a[1].st-1;
  for(int i=2;i<=m;i++){
  	if(mx>a[i].st) dp[i]=a[i].st-1;
  	mx=min(mx,a[i].ed);
  }
  for(int i=1;i<=m;i++){
  	int l=ub(a[i].ed),r;
  	r=ub(a[l].st);
    if(a[r].st!=a[l].st) r--;
  	for(int j=l;j<=r;j++) dp[j]=max(dp[j],dp[i]+a[j].st-a[i].ed-1);
  }
  printf("%d\n",dp[m+1]+n-a[m].ed-1);
  return 0;
}

但是这样只有50分
为什么呢?

经过大量的实验与对拍
我们发现有些任务是不可能完成的
所以那些任务对后面的贡献错误的
而你又没有办法判断那些任务在哪些情况下是不可能完成的
所以会Wa

SPOJ39 PIGBANK - Piggy-Bank-背包

题目描述

传送门
\(n\) 种钱币,有重量,有价值,现在手上的钱包重为 \(m\)
求钱包内最少有多少钱
\(n , m \le 1000\)

Solution

无限背包的板子

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

const int N=505,M=10005,inf=0x3f3f3f3f;
int val[N],cost[N],T,w,x,f[M],n,ans=0;

int main(){
  /*2023.7.24 H_W_Y SP39 PIGBANK - Piggy-Bank 背包*/ 
  scanf("%d",&T);
  while(T--){
  	scanf("%d%d%d",&x,&w,&n);w-=x;
  	for(int i=1;i<=n;i++) scanf("%d%d",&val[i],&cost[i]);
	for(int i=1;i<=w;i++) f[i]=inf;
  	for(int i=1;i<=n;i++)
  	  for(int j=cost[i];j<=w;j++)
	    f[j]=min(f[j],f[j-cost[i]]+val[i]);	
	
	if(f[w]!=inf) printf("The minimum amount of money in the piggy-bank is %d.\n",f[w]);
	else printf("This is impossible.\n");
  }
  return 0;
} 

P1880 [NOI1995] 石子合并-区间DP

题目描述

传送门
\(n\) 堆石子摆成一个环,每次可合并相邻两个石子堆,代价为新石子堆的大小
求合成1堆的最小代价,\(1 \le n \le 300\)

Solution

很明显可以用区间dp来解决
注意区间dp是 \(O(n^3)\)
可以用四边形不等式优化成 \(O(n^2)\)
但是这道题不需要

\(f[l][r]\) 表示合并 \(l \sim r\) 的石子堆的代价
\(f[l][r]=min / max (f[l][k]+f[k+1][r]+sum[r]-sum[l-1])\)

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

const int N=205;
int n,a[N],f[2][N][N],ans[2],s[N];

int main(){
  /*2023.7.24 H_W_Y P1880 [NOI1995] 石子合并 区间DP*/ 
  scanf("%d",&n);
  for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[n+i]=a[i];
  ans[0]=1e9;memset(f[0],0x3f,sizeof(f[0]));
  for(int i=1;i<=n*2;i++) s[i]=s[i-1]+a[i],f[0][i][i]=0;
  for(int len=1;len<=n;len++)
    for(int l=1;l+len-1<=n*2;l++){
      int r=l+len-1;
      for(int k=l;k<r;k++)
        f[0][l][r]=min(f[0][l][r],f[0][l][k]+f[0][k+1][r]+s[r]-s[l-1]),
        f[1][l][r]=max(f[1][l][r],f[1][l][k]+f[1][k+1][r]+s[r]-s[l-1]);
      if(len==n) ans[0]=min(ans[0],f[0][l][r]),ans[1]=max(ans[1],f[1][l][r]);
	}  
  printf("%d\n%d\n",ans[0],ans[1]);
  return 0;
}

P3957 [NOIP2017 普及组] 跳房子

题目描述

传送门

Solution

考虑二分答案
我们在二分过程中再用 \(O(n)\) 的时间进行dp
\(f[i]\) 表示跳到第 \(i\) 个格子的最大得分
每一次转移 \(f[i]=\max f[j](x[i]-d-mid \le x[j] \le x[i]-d+mid) + val[i]\)

考虑如何优化
想到了用两个队列来存储 \(j\)
第一个为单调队列,来存储答案,维护单调递减
第二个为普通的队列,存储 \(x[j]+d-mid \gt x[i]\) 的点
这些点是现在还没有用到的
这样就可以用 \(O(n)\) 完成
总时间复杂度 \(O(n log n)\)

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=5e5+10,inf=0x3f3f3f3f;
int n,d,k,l,r,q[N],lst[N],qh,qt,lh,lt;
bool flag=false;
ll f[N],res;
struct node{
  int x,val;
  bool operator <(const node &rhs)const{return x<rhs.x;}
}a[N];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

bool check(int l,int r){
  int it=0;res=0;
  qh=lh=1;qt=lt=0;
  for(int i=0;i<=n;i++) f[i]=0;
  for(int i=1;i<=n;i++){
    while(lh<=lt&&a[lst[lh]].x<a[i].x-r) lh++;
	while(lh<=lt&&a[lst[lh]].x<=a[i].x-l){
	  while(qh<=qt&&f[lst[lh]]>=f[q[qt]]) qt--;
	  q[++qt]=lst[lh];lh++;
	} 
	while(qh<=qt&&a[q[qh]].x<a[i].x-r) qh++;
	if(qh>qt&&(a[i].x>r||a[i].x<l)) continue;
	f[i]=f[q[qh]]+a[i].val;res=max(res,f[i]);
	lst[++lt]=i;
  }
  return (res>=1ll*k)?true:false;
}

int main(){
  /*2023.7.24 H_W_Y P3957 [NOIP2017 普及组] 跳房子 DP*/ 
  n=read();d=read();k=read();
  for(int i=1;i<=n;i++) a[i].x=read(),a[i].val=read();
  sort(a+1,a+n+1);
  l=0,r=1e9;
  while(l<r){
  	int mid=(l+r)>>1;
  	if(check(max(d-mid,1),d+mid)) r=mid,flag=true;
  	else l=mid+1;
  }
  if(flag) printf("%d\n",l);
  else printf("-1\n");
  return 0;
}

树形DP

P1272 重建道路

题目描述

传送门
给定一颗树,求最少删多少边使其剩一个恰含k个点的树 \(n \le 150\)

Solution

很典型的树形DP
考虑用 \(dp[i][j]\) 表示在第 \(i\) 个节点的子树中
保留 \(j\) 个节点删去的最小边数

那么 \(dp[u][j]= \sum dp[v][k] (\sum k=j)\)
这样对于每一棵子树我们边枚举边统计即可
时间复杂度 \(O(n^3)\)

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

int n,p,head[205],tot=0,dp[205][205],out[205],in[205],root=0;
//dp[i][j]表示以i为根的子树保留j个节点删除的最小边数
struct node{
  int v,next;
}e[505];

void add(int x,int y){
  e[++tot]=(node){y,head[x]};
  head[x]=tot;
}

int dfs(int u){
  int sum=1,now=0;
  for(int i=head[u];i;i=e[i].next){
  	now=dfs(e[i].v);sum+=now;
  	for(int j=sum;j>=1;j--)
  	  for(int k=1;k<j;k++){
  	  	dp[u][j]=min(dp[u][j],dp[u][j-k]+dp[e[i].v][k]-1);
	 }
  	    
  }
  return sum;
}

int main(){
  /*P1272 重建道路 2022.2.19 hewanying*/
  scanf("%d%d",&n,&p);
  for(int i=1,x,y;i<n;i++){
  	scanf("%d%d",&x,&y);
  	out[x]++;in[y]=1;
  	add(x,y);
  }
  memset(dp,0x3f,sizeof(dp));
  for(int i=1;i<=n;i++){
  	if(!in[i]) root=i;
  	dp[i][1]=out[i];
  }
  dfs(root);
  int ans=dp[root][p];
  for(int i=1;i<=n;i++)
    if(dp[i][p]<ans) ans=dp[i][p]+1; 
  printf("%d\n",ans);
  return 0;
}

P1273 有线电视网

题目描述

传送门
给定一颗有根树,每个叶子节点有一个权值,每条边有一个权值
求一颗包含根的树且其叶子节点权值和大于边权和最多包含多少个原树上的叶子节点 \(n \le 3000\)

Solution

和上一道题的dp式子挺像的

H_W_Y-Coding
//远古时期的代码
#include <bits/stdc++.h>
using namespace std;

const int maxn=3005;
int n,m,cnt=0,head[maxn],d[maxn],t[maxn],dp[maxn][maxn],l,r;
struct edge{
  int v,w,next;
}e[2*maxn];

void add(int u,int v,int w){
  e[++cnt]=(edge){v,w,head[u]};
  head[u]=cnt;
}

int dfs(int u,int fa){
  int sum=0;
  if(u>n-m){
  	dp[u][1]=d[u];
  	return 1;
  }
  for(int i=head[u];i;i=e[i].next){
  	int v=e[i].v,w=e[i].w;
  	if(v==fa) continue;
  	int num=dfs(v,u);
  	for(int j=0;j<=sum;j++) t[j]=dp[u][j];
  	for(int j=0;j<=sum;j++)
  	  for(int k=0;k<=num;k++)
  	    dp[u][j+k]=max(dp[u][j+k],t[j]+dp[v][k]-w);
  	sum+=num;    
  }
  return sum;
}

int main(){
  memset(dp,-0x3f,sizeof(dp));
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++) dp[i][0]=0;
  for(int i=1,k,u,w;i<=n-m;i++){
  	scanf("%d",&k);
  	for(int j=1;j<=k;j++){
  	  scanf("%d%d",&u,&w);
	  add(i,u,w);add(u,i,w);	
	}
  }
  for(int i=1;i<=m;i++) scanf("%d",&d[n-m+i]);
  dfs(1,0);
  for(int i=m;i>=0;i--)
  	if(dp[1][i]>=0){
  	  printf("%d\n",i);
	  break;	
	}
  return 0;
}

P2458 [SDOI2006] 保安站岗

题目描述

传送门
给定一颗树,每个点有一个权值,
对于一个点,若存在一条边使另一个点被选,则这个点可不选
求最小被选权值 \(n \le 5000000\)

Solution

考虑对于每一个点都有三种情况
自己的儿子选,自己选和父亲选
注意儿子选时只用选一个就可以了

考虑用 \(f[u][0/1/2]\) 分别来表示这三种情况
则可以得到
\(f[u][0]=\min(f[v][1]- \min(f[v][0],f[v][1]))+ \sum \min(f[v][0/1])\)
\(f[u][1]=a[u]+\sum \min(f[v][0/1/2])\)
\(f[u][2]=\sum \min(f[v][0/1])\)

然后就做完了

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

const int maxn=1e4+10;
int n,head[maxn],tot=0,f[maxn],rt,m,a[maxn],id,x,dp[maxn][3];
struct edge{
  int v,nxt;
}e[maxn*2];

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};
  head[u]=tot;
}

void dfs(int u,int fa){
  int res=0x3f3f3f3f;
  dp[u][0]=0;dp[u][1]=a[u];dp[u][2]=0;
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa) continue;
  	dfs(v,u);
  	res=min(res,dp[v][1]-min(dp[v][0],dp[v][1]));
  	dp[u][0]+=min(dp[v][0],dp[v][1]);
  	dp[u][1]+=min(dp[v][1],min(dp[v][2],dp[v][0]));
  	dp[u][2]+=min(dp[v][1],dp[v][0]);
  }
  dp[u][0]+=res;
}

int main(){
  /*2023.7.26 H_W_Y P2458 [SDOI2006] 保安站岗 树形DP*/ 
  scanf("%d",&n);
  for(int i=1;i<=n;i++) f[i]=i;
  for(int i=1;i<=n;i++){
  	scanf("%d",&id);scanf("%d%d",&a[id],&m);
  	for(int j=1;j<=m;j++) scanf("%d",&x),f[x]=id,add(id,x);
  }
  for(int i=1;i<=n;i++)
    if(f[i]==i){rt=i;break;}
  dfs(rt,0);
  printf("%d\n",min(dp[rt][0],dp[rt][1]));
  return 0;
} 

CF1324F Maximum White Subtree

题目描述

传送门
给定一棵 \(n\) 个节点无根树,每个节点要么黑,要么白
对于每个节点,求包含它的连通子图白点数与黑点数的差最大是多少
\(n \le 5000000\)

Solution

首先考虑到一个树型DP的经典做法
进行两边 dfs,第一遍统计子树的答案,第二遍统计父亲的答案

在这道题中,我们同样用这样的方法
考虑先统计子树的答案
如果 \(u\) 的儿子节点 \(v\) 的贡献大于0
那么就可以加上 \(v\) 的贡献

紧接着是统计父亲节点贡献的答案
发现父亲的答案 \(f[u]\) 与子树的答案 \(f[v]\) 是有一定关系的
如果此时 \(f[v]>0\) 那么 \(v\) 的答案已经贡献到 \(u\) 里面了
所以 \(f[v]=\max(f[v],f[u])\)
反之,在 \(f[u]\) 中没有 \(v\) 的成分
所以 \(f[v]=\max(f[v],f[v]+f[u])\)
要把这里的关系想清楚才行

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

const int maxn=2e5+10;
int n,head[maxn],tot=0,f[maxn];
struct edge{
  int v,nxt;
}e[maxn<<1];

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};
  head[u]=tot;
  e[++tot]=(edge){u,head[v]};
  head[v]=tot;
}

void dfs1(int u,int fa){
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa) continue;
  	dfs1(v,u);
  	if(f[v]>0) f[u]+=f[v];
  }
}

void dfs2(int u,int fa){
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa) continue;
  	if(f[v]>0) f[v]=max(f[v],f[u]);
  	else f[v]=max(f[v],f[v]+f[u]);
  	dfs2(v,u);
  }
}

int main(){  
  /*2023.7.26 H_W_Y CF1324F Maximum White Subtree 树形DP*/ 
  scanf("%d",&n);
  for(int i=1;i<=n;i++) scanf("%d",&f[i]),f[i]=(f[i]==1)?1:-1;
  for(int i=1,u,v;i<n;i++) scanf("%d%d",&u,&v),add(u,v);
  dfs1(1,0);dfs2(1,0);
  for(int i=1;i<=n;i++) printf("%d ",f[i]);
  printf("\n");
  return 0;
}

P2607 [ZJOI2008] 骑士

题目描述

传送门
\(n\) 个骑士,每个骑士有一个最讨厌的骑士和战斗力
骑士与他讨厌的骑士不能同时被选,求最多战斗力 \(n \le 1000000\)

Solution

发现题目中给出的是一棵基环树
(第一次做拆环的题)

但是要注意不是只有一个环的,所以我们考虑对于每一个环分别处理
答案就是这些之和即可

我们可以先建立一个有向图
考虑到每一个点的入度都为1
所以是一定存在环的
我们就对于每一个未访问的点去找环

而在找到了环之后
我们考虑强制这个点不选
进行一次dfs

再找到这个点的父亲,也就是环中与其相邻的点
再进行一次同样的dfs即可
这样就相当于拆掉了两次dfs根的这一条边
至于dp方程就是和没有上司的舞会一样的

简单来说这道题就是把点和环分别舞会再求和

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e6+5;
int n,head[N],tot=0,fa[N];
ll f[N][2],ans=0,a[N];
bool vis[N];
struct edge{
  int v,nxt;
}e[N<<1];

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};
  head[u]=tot;
}

void dfs(int u,int pre){
  vis[u]=true;
  f[u][0]=0;f[u][1]=a[u];
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v!=pre){
  	  dfs(v,pre);
  	  f[u][0]+=max(f[v][0],f[v][1]);
  	  f[u][1]+=f[v][0];
	}
	else f[v][1]=-1e9;
  }
}

void find(int u){
  while(!vis[u]){
  	vis[u]=true;
  	u=fa[u];
  }//找环
  dfs(u,u);
  ll res=max(f[u][0],f[u][1]);
  u=fa[u];
  dfs(u,u);
  res=max(res,max(f[u][0],f[u][1]));
  ans+=res;
}

int main(){
  /*2023.7.27 H_W_Y P2607 [ZJOI2008] 骑士 树形DP*/ 
  scanf("%d",&n);
  for(int i=1,x;i<=n;i++){
  	scanf("%d%d",&a[i],&x);
  	add(x,i);fa[i]=x;
  }
  for(int i=1;i<=n;i++)
    if(!vis[i]) find(i);
  printf("%lld\n",ans);
  return 0; 
}

CF767C Garland

题目描述

传送门
给定一棵 \(n\)个节点的带权树,
删除一些边使它变成三个权值相同的树 \(n \le 1000000\)

Solution

考虑进行一次遍历
如果遇到一棵子树的siz为总权值的三分之一
那么就可以记录一下

这样就会有两种合法情况:

  1. 有两棵子树的siz都为总权值的三分之一
  2. 有一棵子树的siz为总权值的三分之二,而这棵子树的子树siz为总权值的三分之一

我们考虑如何把这两种情况合并一下
发现我们可以在记录答案的同时把siz赋为0
这样就不会影响后面的枚举
也就是相当于一边枚举一边删除

同时还要注意根节点不能成为答案

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

const int N=2e6+5;
int n,a[N],sum=0,head[N],tot=0,cnt=0,ans[N],rt,siz[N];
bool flag=false;
struct edge{
  int v,nxt;
}e[N<<1];

void add(int u,int v){
  e[++tot]=(edge){v,head[u]};
  head[u]=tot;
  e[++tot]=(edge){u,head[v]};
  head[v]=tot;
}

void dfs(int u,int fa){
  siz[u]=a[u];
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==fa) continue;
  	dfs(v,u);
  	siz[u]+=siz[v];
  }
  if(siz[u]==sum/3&&u!=rt) ans[++cnt]=u,siz[u]=0;
}

int main(){
  /*2023.7.27 H_W_Y CF767C Garland 树形DP*/ 
  scanf("%d",&n);
  for(int i=1,x;i<=n;i++){
  	scanf("%d%d",&x,&a[i]);
  	sum+=a[i];
  	if(x) add(i,x);
  	else rt=i;
  }
  dfs(rt,0);
  if(cnt>=2&&sum%3==0) printf("%d %d\n",ans[1],ans[2]);
  else printf("-1\n");
  return 0;
}

数位DP

P4127 [AHOI2009] 同类分布

题目描述

传送门
求出 [a,b] 中各位数字之和能整除原数的数的个数 \(a,b ≤ 1e18\)

Solution

对于这种求是否能整除的题
我们只有在最后才能得到答案

这道题很明显是数位DP
考虑用记忆化搜索来实现
对于每一位我们需要维护前面的数字之和和前面所组成的数
而由于前面所组成的数太大了,可以到达\(1e18\)
但是数字之和又一定\(\le 9* 18\)

我们就考虑取模数
这样进行\(9 * 18\)次dfs即可
每一次要判断数字之和\(=mod\)
这样记录答案就可以了

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll a,b,mod,dp[20][185][185];
int len=0,p[20];

ll dfs(int id,int sum,ll st,int limit){
  if(id>len) return st==0&&sum==mod?1:0;
  if(!limit&&dp[id][sum][st]!=-1) return dp[id][sum][st];
  int u=limit?p[id]:9;
  ll res=0;
  for(int i=0;i<=u;i++)
    res+=dfs(id+1,sum+i,(st*10+i)%mod,limit&&i==u);
  if(!limit) dp[id][sum][st]=res;
  return res;
}

ll solve(ll x){
  len=0;
  while(x){
  	p[++len]=x%10;
  	x/=10;
  }
  ll res=0;
  for(int i=1;i<=len/2;i++) swap(p[i],p[len-i+1]);
  for(mod=1;mod<=9*len;mod++){
    memset(dp,-1,sizeof(dp));
    res+=dfs(1,0,0,1);
  }
  return res;
}

int main(){
  /*2023.7.19 H_W_Y P4127 [AHOI2009] 同类分布 数位DP*/ 
  scanf("%lld%lld",&a,&b);
  printf("%lld\n",solve(b)-solve(a-1));
  return 0;
}
posted @ 2023-07-20 14:20  H_W_Y  阅读(15)  评论(0编辑  收藏  举报