" 堪 比 地 狱 "

DP专项挑战

50+道DP题整理,祝好! 同时也会有DP做题经验的总结

001【NOIP 2017】宝藏

状态压缩DP,因为n的范围很小,设状态s表示已经到达了哪些屋子

不好转移:代价\(L\times K\)中的K需要记录

我们可以将题目看作要求找出一棵生成树,这样就K就是节点的深度,将其维护进dp数组

得到状态dp[s][i]表示当前状态为s,深度为i,为方便状态转移将节点编号范围变为0到n-1

预处理出每个状态能到达的下一个状态,边界为初始免费的屋子k,将所有dp[1<<k][0]设为0

枚举状态s,进而枚举s的子集son,计算 s^son,则集合 son+s^son=s

枚举s^son中的元素能到son中元素的最短距离,将其累加最后乘上深度就是当前这次转移的代价

\(dp[s][j]=min(dp[s][j],dp[son][j-1]+sum*j)\)

trick one:数据范围小想到状压DP

trick two:将不好搞且需要记录的东西维护进dp数组(此题中的k)

AC code

点击查看代码
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<climits>
#include<algorithm>
#include<cstdio>
#define int long long

using namespace std;

const int maxn=1<<15;
const int INF=0x3f3f3f3f;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int dp[maxn][16];
int state[maxn];
int n,m,dis[16][16];

signed main()
{
	n=read();m=read();
	
	memset(dis,0x3f,sizeof(dis));
	
	for(int i=1;i<=m;i++)
	{
		int u=read()-1;
		int v=read()-1;
		int c=read();
		dis[u][v]=min(c,dis[u][v]);
		dis[v][u]=min(c,dis[v][u]);
	}
	
	memset(dp,0x3f,sizeof(dp));
	
	for(int i=1;i<=(1<<n)-1;i++)
	{
		for(int j=0;j<n;j++)
		{
			if((i|(1<<j))==i)
			{
				dis[j][j]=0;
				for(int k=0;k<n;k++)
				{
					if(dis[j][k]!=INF) state[i]|=(1<<k);
				}
			}
		}
	}
	
	for(int i=0;i<n;i++)
	{
		dp[1<<i][0]=0;
	}
	
	for(int i=2;i<=(1<<n)-1;i++)
	{
		for(int j=i-1;j;j=(j-1)&i)
		{
			if((state[j]|i)==state[j])
			{
				int sta=(j^i);
				int sum=0;
				for(int k=0;k<n;k++)
				{
					if((1<<k)&sta)
					{
						int len=INF;
						for(int l=0;l<n;l++)
						{
							if(j&(1<<l))
							{
								len=min(len,dis[l][k]);
							}
						}
						sum+=len;
					}				
				}
				for(int k=1;k<n;k++)
				{
					if(dp[j][k-1]!=INF)
					{
						dp[i][k]=min(dp[i][k],dp[j][k-1]+sum*k);
					}
				}	
			}
		}
	}
	
	int ans=INF;
	
	for(int i=0;i<n;i++)
	{
		ans=min(ans,dp[(1<<n)-1][i]);
	}	
	
	cout<<ans;
	
	return 0;
} 

002【POI 2013】LUK-Triumphal arch

树形DP,很有思维量的一道题,要求求出的是k的最小值,而可以发现k可以二分,所以我们对k进行二分

很显然的(我这样的【】都能看出来)我们每次移动B之前应该把B的所有子节点全都染成黑色

设dp[i]表示将i的子树(不含i)全部染成黑色所需的求救染色的节点的个数,什么是求救染色节点个数呢?当一个节点的儿子多于k个的时候,就需要求救。谁来救他呢?我们发现,要尽可能的阻止B,应该是从根节点到子节点这样染色,所以对他施以援手的是一个祖先节点,如果某一祖先节点的儿子数量小于k,那就会有剩余,就可以提前把现在的不够的节点染色,而现在不够染色的节点数量极为求救染色节点个数

设遍历到某一节点的儿子数为son[i],则如果\(son[i]-k>0\),则\(son[i]-k\)为求救节点个数,如果\(son[i]-k<0\),则\(son[i]-k\)的绝对值为能够救援的节点的个数,可以拿它与求救节点的个数相抵消

转移方程\(dp[i]=son[i]-k+\sum max(dp[to],0)\)(to为儿子节点),这里的零要注意理解,因为儿子无法对父亲及祖先伸出援手,即使有剩余也无济于事,只能当做虚无

最后经过抵消之后得到的dp[1]即为无法救助的节点数量,即每次染k个节点,最少最少还要剩下dp[1]个节点,如果dp[1]大于0,说明没救了,二分的k不合法,否则说明k是合法的

trick three:求最值/方案数,考虑DP

trick four:看到最小值判断单调性,进而考虑二分

trick five:如果想不出合适的状态,可以去努力挖掘题目的性质,如本题中的剩余与求救,本质上就是子节点数量与k的差值

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>

using namespace std;

const int maxn=3e5+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,tot;
int dp[maxn];
int cnt[maxn];
int head[maxn];
struct edge
{
	int next,to;
}e[maxn*2];

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

void dfs_first(int x,int fa)
{
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dfs_first(to,x);
		cnt[x]++;
	}
}

void dfs(int x,int f,int k)
{
	dp[x]=cnt[x]-k;
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==f) continue;
		dfs(to,x,k);
		dp[x]+=max(dp[to],0);
	}
}

int main()
{
	n=read();
	
	for(int i=1;i<n;i++)
	{
		int u=read();
		int v=read();
		add(u,v),add(v,u);
	}
	
	dfs_first(1,0);
	
	int l=0,r=3e5,ans;
	
	while(l<=r)
	{
		int mid=(l+r)>>1;
		dfs(1,0,mid);
		if(dp[1]<=0)
		{
			r=mid-1;
			ans=mid;
		}
		else
		{
			l=mid+1;
		}
	}
	
	cout<<ans;
	
	return 0;
}

003【IOI 2005】Riv 河流

树形DP+背包DP,IOI经典题目

自己想的话,很容易设状态dp[i][j]表示在以i为根节点的子树内,建造j个场子,并不好转移

我们要知道每个村庄木料传送的代价,我们必须还要知道,他们会在哪里game over,即会被送到哪个场子去

也就是说上面的状态就是缺少了他们的最终归宿,借鉴第一题的trick,可以将这个不好维护的最终归宿维护进dp数组的下标内

dp[i][j][k]表示以i为根节点的子树内,i的最终归宿是j,建造k个场子的最小代价,即j是i最近的一个祖先

然而如果是在i号节点本身建一个场子呢?混在一起可能就不是很好转移,所以我们另开一个dp数组,表示在i号节点建场子的最小代价,也就是统计其他的节点的最终归宿是i的情况,在每次递归结束后进行这种情况向第一个dp数组的dp[i][i][k]合并

这样一来在递归过程中产生了一个背包的模型,在i的子树内选几个节点建场子,让代价尽量小,最终的答案为dp[0][0][k],由于此题可能不是很好理解,代码有注释

trick six:注意多种DP混合在一起的情况

trick seven:有时需要开两个数组对答案进行统计合并

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>

using namespace std;

const int maxn=110;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int cnt;
int n,k,tot;
int w[maxn];
int f[maxn];
int d[maxn];
int sta[maxn]; //为了方便转移,用栈来存储祖先 
int deep[maxn];
int head[maxn];
int dp_f[maxn][maxn][maxn];
int dp_s[maxn][maxn][maxn];

struct edge
{
	int to,next;
}e[maxn*2];

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

void dfs(int x)
{
	sta[++cnt]=x;
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		deep[to]=deep[x]+d[to];
		dfs(to);
		for(int i=1;i<=cnt;i++)
		{
			for(int j=k;j>=0;j--)
			{
				dp_f[x][sta[i]][j]+=dp_f[to][sta[i]][0];
				dp_s[x][sta[i]][j]+=dp_f[to][x][0];
				//这两种情况没有必要取min值,因为肯定是全都需要送到sta[i]或x处的 
				for(int l=1;l<=j;l++)
				{
					dp_f[x][sta[i]][j]=min(dp_f[x][sta[i]][j],dp_f[to][sta[i]][l]+dp_f[x][sta[i]][j-l]);
					dp_s[x][sta[i]][j]=min(dp_s[x][sta[i]][j],dp_f[to][x][l]+dp_s[x][sta[i]][j-l]);
				}
			}
		}
	}
	
	for(int i=1;i<=cnt;i++)
	{
		for(int j=k;j>=1;j--)
		{
			dp_f[x][sta[i]][j]=min(dp_f[x][sta[i]][j]+w[x]*(deep[x]-deep[sta[i]]),dp_s[x][sta[i]][j-1]); //一目了然 
		}
		dp_f[x][sta[i]][0]+=w[x]*(deep[x]-deep[sta[i]]); //这个deep用了一个前缀和思想 
	}
	//将x节点的贡献统计进来,而他的子树的贡献在上面的三层for循环以及之前的递归中统计过了 
	
	cnt--;
}

int main()
{
	n=read(),k=read();
	
	for(int i=1;i<=n;i++)
	{
		w[i]=read();
		f[i]=read();
		d[i]=read();
		add(f[i],i);
	}
	
	dfs(0);
	
	cout<<dp_f[0][0][k];
	
	return 0;
}

004【COCI 2014-2015#1】Kamp

这玩应真特么难我草 题意挺好理解,以每个点为根暴力分50pts,时间复杂度\(O(n^2)\),正解很明显要优化掉枚举根节点的\(O(n)\)复杂度,所以考虑换根

然后就后边的就想不到了

设g[i]表示搞定以i为根的子树内的人然后回到i需要的花费,f[i]为搞定子树外的人的花费

很明显我们送完最后一个人就可以停止了,所以每个节点的人产生贡献应该有一去一回,而最后一个人不用回,贪心的想最后一个人肯定是到根节点的最远的一个人

我们分别预处理在以i为根的子树内,离i最远的人有多远,离i第二远的人有多远,然后再处理出在子树外离i最远的人有多远

然后分类讨论,\(ans_i\)\(g_i+f_i-max(子树内最远的人,子树外最远的人)\),代码有注释

trick eight:考场上首先考虑部分分

trick nine:树形DP由\(O(n^2)\)\(O(n)\)可以考虑换根

trick ten:实在想不出来就放弃吧,骗骗部分分就行了

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=5e5+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,k,tot;
int f[maxn];
int g[maxn];
int up[maxn];
int cnt[maxn];
int head[maxn];
bool mark[maxn];
int dis[maxn][2];
struct edge
{
	int to,next,val;
}e[maxn*2];

void add(int x,int y,int z)
{
	e[++tot].to=y;
	e[tot].val=z;
	e[tot].next=head[x];
	head[x]=tot;
}

void dfs(int x,int fa)
{
	if(mark[x]) cnt[x]++; //统计子树内有多少个人 
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==fa) continue;
		dfs(to,x);
		if(!cnt[to]) continue; //子树里没有人,也没啥意义了 
		g[x]+=g[to]+2*e[i].val; //子树to内节点对x的贡献(一来一回的) 
		int now=dis[to][0]+e[i].val; //现在的子树内的最远距离 
		if(now>dis[x][0]) dis[x][1]=dis[x][0],dis[x][0]=now; //明显是最远的,更新次远与最远 
		else if(now>dis[x][1]) dis[x][1]=now; //更新次远的 
		cnt[x]+=cnt[to]; //统计一下子树内人的个数 
	}
}

void dp(int x,int fa)
{
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==fa) continue;
		if(k-cnt[to]) //子树外有人,统计一下子树外的贡献 
		{
			f[to]=f[x]+g[x]-g[to]; 
			//此时x在to的子树外,g[x]-g[to]是x的子树内却不在to的子树内的点的贡献 
			if(!cnt[to]) f[to]+=2*e[i].val;
			//子树内没有人,这条边是没有对g[x]产生贡献的,却对f[to]产生了贡献(画图理解即可) 
			if(dis[to][0]+e[i].val==dis[x][0]) //如果to是在x子树内最远的路径上的点
				up[to]=max(up[x],dis[x][1])+e[i].val; //拿x子树内第二远的去更新to子树外的最远距离 
			else
				up[to]=max(up[x],dis[x][0])+e[i].val; //否则拿x子树内最远距离更新to子树外最远距离
			//这里挺好理解的,画画图就出来了 
		}
		dp(to,x);
	}
}

signed main()
{
	n=read(),k=read();
	
	for(int i=1;i<n;i++)
	{
		int u=read();
		int v=read();
		int w=read();
		add(u,v,w);
		add(v,u,w);
	}
	
	for(int i=1;i<=k;i++)
	{
		int k=read();
		mark[k]=true; //标记一下 
	}
	
	dfs(1,0); //预处理 
	
	dp(1,0); //第二遍dfs,进行统计 
	
	for(int i=1;i<=n;i++)
	{
		cout<<f[i]+g[i]-max(up[i],dis[i][0])<<endl; 
		//答案为子树内外来回的贡献-离它最远的那个点的距离 
	}

	return 0;
}

005【ZJOI 2010】数字计数 /【SCOI 2009】windy数

俩题算一道吧反正也挺水的 数位DP模板题 强推记忆化搜索

AC code 数字计数

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=15;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int a,b,num[maxn];
int dp[maxn][2][maxn][2];

int dfs(int len,bool lim,int sum,bool zero,int c)
{
	int ans=0;
	if(!len) return sum;
	if(dp[len][lim][sum][zero]!=-1) return dp[len][lim][sum][zero];
	for(int i=0;i<=9;i++)
	{
		if(lim==1 && i>num[len]) break;
		ans+=dfs(len-1,lim && (i==num[len]),sum+((i==c) && (i || !zero)),zero && (i==0),c);
	}
	dp[len][lim][sum][zero]=ans;
	return ans;
}

int solve(int x,int c)
{
	int len=0;
	while(x)
	{
		num[++len]=x%10;x/=10;
	}
	memset(dp,-1,sizeof(dp));
	dfs(len,1,0,1,c);
}

signed main()
{
	scanf("%lld%lld",&a,&b);
	
	for(int i=0;i<=9;i++)
	{
		cout<<solve(b,i)-solve(a-1,i)<<" ";
	}
	
	return 0;
}

AC code windy数

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=15;

int a,b,num[maxn];
int dp[maxn][2][maxn][2];

int dfs(int len,bool lim,int last,bool zero)
{
	int ans=0;
	if(len==0) return 1;
	if(dp[len][lim][last][zero]!=-1) return dp[len][lim][last][zero];
	for(int i=0;i<=9;i++)
	{
		if(lim && i>num[len]) break;
		if(abs(i-last)<2 && !zero) continue;
		ans+=dfs(len-1,lim && (i==num[len]),i,(zero && i==0));
	}
	dp[len][lim][last][zero]=ans;
	return ans;
}

int solve(int x)
{
	int len=0;
	while(x)
	{
		num[++len]=x%10;x/=10;
	}
	memset(dp,-1,sizeof(dp));
	return dfs(len,1,11,1);
}

signed main()
{
	scanf("%lld%lld",&a,&b);
	
	cout<<solve(b)-solve(a-1);
	
	return 0;
}

006【POI 2014】PTA-Little Bird

相当于每只鸟每次都要选一个最小的树,而区间长度也已经给出

明显单调队列优化DP,挺好想的,按题目来就行了

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<deque>
#include<algorithm> 

using namespace std;

const int maxq=30;

const int maxn=1e6+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,d[maxn];
int q,k[maxq];
int dp[maxn];
int qu[maxn];
int head,tail;

int main()
{
	n=read();
	
	for(int i=1;i<=n;i++)
	{
		d[i]=read();
	}
	
	q=read();
	
	for(int i=1;i<=q;i++)
	{
		k[i]=read();
	}
	
	for(int i=1;i<=q;i++)
	{
		qu[tail=head=1]=1;
		for(int j=2;j<=n;j++)
		{
			while(head<=tail && j-qu[head]>k[i])
			{
				head++;
			}
			dp[j]=dp[qu[head]]+(d[j]>=d[qu[head]]);
			while(head<=tail && dp[j]<dp[qu[tail]] || (dp[j]==dp[qu[tail]] && d[qu[tail]]<=d[j]))
			{
				tail--;
			}
			qu[++tail]=j;
		}
		cout<<dp[n]<<endl;
	}
	
	return 0;
}

007【CQOI 2016】手机号码

数位DP,按模板来就好,维度有点多

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=15;

long long l,r;
int num[maxn];
long long dp[maxn][maxn][maxn][2][2][2][2];

//len pre ppre sum lim four eight tree_num

int dfs(int len,int pre,int ppre,bool sum,bool lim,bool four,bool eight)
{

	int ans=0; if(four && eight) return 0; if(len==0) return sum;
	if(dp[len][pre][ppre][sum][lim][four][eight]!=-1) return dp[len][pre][ppre][sum][lim][four][eight];
	for(int i=0;i<=9;i++)
	{
		if(lim && i>num[len]) break;
		ans+=dfs(len-1,i,pre,sum||(i==pre&&i==ppre),lim&&i==num[len],four||(i==4),eight||(i==8));
	}	
	dp[len][pre][ppre][sum][lim][four][eight]=ans;
	return ans;
}

int solve(int x)
{
	if(x<1e10) return 0;
	int ans=0;
	int len;
	for(len=0;x;x/=10) num[++len]=x%10;
	memset(dp,-1,sizeof(dp));
	for(int i=1;i<=num[len];i++)
	{
		ans+=dfs(10,i,0,0,(i==num[len]),i==4,i==8);
	}
	
	return ans;
}

signed main()
{
	scanf("%lld%lld",&l,&r);
	cout<<solve(r)-solve(l-1)<<endl;
	return 0;
 } 

008【HNOI 2002】Tinux系统

树形结构,记忆化搜索,可以贪心的先按照从小到大的顺序排序,优先使用耗时较少的

然后记忆化搜索,分为叶节点和非叶节点,递归求解,每次仅剩一个可以使用的指针时应该给非叶节点,以保证安排完全部的文件

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxp=160;
const int maxn=1010;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,k;
int p[maxp];
int dp[maxn][maxn];

int dfs(int x,int y)
{
	if(x==1)
	{
		return dp[x][y]=p[y];
	}
	if(y==k) return dp[x][y]=p[y]*x*x+dfs(x,1);
	if(dp[x][y]) return dp[x][y];
	dp[x][y]=dfs(x-1,y+1)+p[y];
	for(int i=2;i<x;i++)
	{
		dp[x][y]=min(dp[x][y],dfs(i,1)+dfs(x-i,y+1)+p[y]*i*i);
	}
	return dp[x][y];
}

signed main()
{
	n=read(),k=read();
	
	for(int i=1;i<=k;i++)
	{
		p[i]=read();
	}
	
	sort(p+1,p+k+1);
	
	cout<<dfs(n,1);
	
	return 0;
}

009【CQOI 2009】叶子的染色

树形DP,dp[x][0/1]代表该节点染成黑/白后的最小代价,我们可以随便搞一个根节点,然后初始化时将所有节点从无色变为有色的代价置成1,直接将无法到达的状态(叶节点变成给出的颜色对立的颜色)置为INF,剩下的状态是合法状态或可以向最优解转移的中间状态,然后dfs

可以发现根节点一定会被染色所以答案是根节点黑白两色中代价较小的一中

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<climits>
#include<cmath>
#include<algorithm>

using namespace std;

const int maxn=1e4+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,m,tot,root;
int dp[maxn][2];
int head[maxn];
struct edge
{
	int to,next;
}e[maxn*2];
int cur[maxn];

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

void dfs(int x,int f)
{
	if(x<=n) return ;
	for(int i=head[x];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to==f) continue;
		dfs(to,x);
		dp[x][0]+=min(dp[to][0]-1,dp[to][1]);
		dp[x][1]+=min(dp[to][1]-1,dp[to][0]);
	}
} 

int main()
{
	m=read();
	n=read();
	root=n+1;
	for(int i=1;i<=n;i++) cur[i]=read();
	
	for(int i=1;i<m;i++)
	{
		int u=read();
		int v=read();
		add(u,v),add(v,u);
	}
	
	for(int i=1;i<=m;i++)
	{
		dp[i][0]=dp[i][1]=1;
		if(i<=n) dp[i][!cur[i]]=INT_MAX;
	}
	
	dfs(root,0);
	
	cout<<min(dp[root][0],dp[root][1]);
	
	return 0;
}

010【GDOI 2014】拯救莫莉斯

\(n\times m\leq 50\)\(m\leq n\)可得\(m\leq 8\),因此可得状态压缩

也挺板的,然后也有点新意,依旧是枚举这一行的和上一行的状态转移,转移过程按题目说的一点点转移

合法转态:设当前为第i行,则设i行状态为j,i-1行状态为k,i-2行状态为l

(j | k | l | (k<<1) | (k>>1))&(1<<m-1)=(1<<m-1)时合法

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<climits>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=55;
const int maxm=(1<<8);

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,m;
int f[maxn][10];
int val[maxn][maxm];
int dp_1[maxn][maxm][maxm]; 
int dp_2[maxn][maxm][maxm];

int count(int x)
{
	int ans=0;
	for(;x;x>>=1)
		if(x&1) ans++;
	return ans;
}

signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			f[i][j]=read();

	int lim=(1<<m)-1;

	for(int i=1;i<=n;i++)
		for(int j=0;j<=lim;j++)
			for(int k=0;k<m;k++)
				if(j&(1<<k)) val[i][j]+=f[i][k+1];
	
	memset(dp_1,0x3f3f3f,sizeof(dp_1));
	
	for(int i=0;i<=lim;i++)
	{
		dp_1[1][i][0]=val[1][i],dp_2[1][i][0]=count(i);
	}
	
	for(int i=2;i<=n;i++)
	{
		for(int j=0;j<=lim;j++)
		{
			for(int k=0;k<=lim;k++) 
			{
				for(int l=0;l<=lim;l++) 
				{
					if(((j | k | l | (k<<1) | (k>>1))&lim)==lim)
					{
						int v=val[i][j]+dp_1[i-1][k][l];
						int num=count(j)+dp_2[i-1][k][l];
						if(v<dp_1[i][j][k] || v==dp_1[i][j][k] && num<dp_2[i][j][k])
							dp_1[i][j][k]=v,dp_2[i][j][k]=num;		
					}
				}
			}
		}					
	}
	
	int ans_1=INT_MAX,ans_2=INT_MAX;
	
	for(int i=0;i<=lim;i++) 
	{
		for(int j=0;j<=lim;j++) 
		{
			if(((j | i | (i<<1) | (i>>1))&lim)==lim)
			{
				if(ans_1>dp_1[n][i][j] || ans_1==dp_1[n][i][j] && ans_2>dp_2[n][i][j])
					ans_1=dp_1[n][i][j],ans_2=dp_2[n][i][j];
			}
		}	
	}		
	
	cout<<ans_2<<" "<<ans_1;
	
	return 0;
}

011 SERVICE - Mobile Service

设计四维状态dp[i][j][k][l]表示第i个任务,三个人位置j,k,l的花费

会MLE,滚动数组,一定有一个人在当前的这个任务的位置

所以dp[i][j][k]为i个人两个不在该任务位置的人在j,k位置

然后大力分讨,从第i+1个任务转移

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<climits>
#include<algorithm>

using namespace std;

const int maxn=1010;
const int maxl=210; 

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int t,l,n;
int p[maxn];
int c[maxl][maxl];
int dp[maxn][maxl][maxl];

int main()
{
	t=read();
	
	while(t--)
	{
		l=read(),n=read();
	
		for(int i=1;i<=l;i++)
		{
			for(int j=1;j<=l;j++)
			{
				c[i][j]=read();
			}
		}
	
		for(int i=1;i<=n;i++)
			p[i]=read();
	
		memset(dp,0x3f3f3f,sizeof(dp));
		
		dp[0][1][2]=0;p[0]=3;
		
		for(int i=0;i<n;i++)
		{
			for(int a=1;a<=l;a++)
			{
				for(int b=1;b<=l;b++)
				{
					if(a==b || a==p[i] || b==p[i]) continue;
					dp[i+1][p[i]][b]=min(dp[i+1][p[i]][b],dp[i][a][b]+c[a][p[i+1]]);
					dp[i+1][a][p[i]]=min(dp[i+1][a][p[i]],dp[i][a][b]+c[b][p[i+1]]);
					dp[i+1][a][b]=min(dp[i+1][a][b],dp[i][a][b]+c[p[i]][p[i+1]]);	
				}
			}
		}
		
		int ans=INT_MAX;
	
		for(int i=1;i<=l;i++)
		{
			for(int j=1;j<=l;j++)
			{
				ans=min(ans,dp[n][i][j]);
			}
		}
	
		cout<<ans<<endl;
	}
	
	return 0;
}

012【SCOI 2009】粉刷匠

背包好题 因为粉刷次数是题目中有关的量,但是又很难维护,因此搞进下标

其实可以每次单独考虑当前这一行 设两个状态

dp[i][j]表示前i行刷j次最多的格子

f[i][j][k]表示第i行刷j次刷到第k个格子的最多正确的格子数

用个前缀和维护一下每一行应该为蓝色格子的数量,然后红色格子也就知道了

转移挺好转移的 主要是状态不好设计

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>

using namespace std;

const int N=55;
const int maxn=2510;

int n,m,t;
char b[N][N];
int sum[N][N];
long long ans;
int f[N][maxn][N];
long long dp[N][maxn];

int main()
{
	scanf("%d%d%d",&n,&m,&t);
	
	for(int i=1;i<=n;i++)
	{
		scanf("%s",b[i]+1);
	}
	
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			sum[i][j]=sum[i][j-1]+b[i][j]-'0';
		}
	}
	
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int k=1;k<=m;k++)
				for(int q=j;q<=k;q++)
					f[i][j][k]=max(f[i][j][k],f[i][j-1][q-1]+max(sum[i][k]-sum[i][q-1],k-q+1-sum[i][k]+sum[i][q-1]));
		
	for(int i=1;i<=n;i++)
		for(int j=1;j<=t;j++)
			for(int k=1;k<=min(j,m);k++)
				dp[i][j]=max(dp[i][j],dp[i-1][j-k]+f[i][k][m]);
			
	for(int i=1;i<=t;i++) ans=max(ans,dp[n][i]);
		
	cout<<ans; 
		
	return 0;
}

013 RGB Sequence

这题也挺难想的(曾经的国家集训队作业),设dp[i][j][k]表示当前位置为i,然后其他两种颜色的格子最后出现的位置为j,k(默认j为靠后的),可以把无法转移过来的状态设为0,然后向后转移

分为三种情况

  1. i+1颜色同i,dp[i+1][j][k]+=dp[i][j][k]

  2. i+1颜色同j,dp[i+1][i][k]+=dp[i][j][k]

  3. i+1颜色同k,dp[i+1][i][j]+=dp[i][j][k]

这里并未规定每个点必须为何种颜色,每个位置有三种情况 ans最后要乘3

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<vector>
#include<algorithm>
#define int long long
#define pii pair<int,int>

using namespace std;

const int maxn=310;
const int mod=1e9+7;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,m,ans;
vector <pii> lim[maxn];
int dp[maxn][maxn][maxn];

signed main() 
{
	n=read(),m=read();
	for(int i=1;i<=m;i++)
	{
		int l=read();
		int r=read();
		int c=read();
		lim[r].push_back(make_pair(l,c));
	}
	
	dp[1][0][0]=1;
	
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<lim[i].size();j++) //枚举限制 
		{
			int l=lim[i][j].first;
			int cnt=lim[i][j].second;
			for(int k=0;k<i;k++)
			{
				for(int s=0;s<=max(0ll,k-1);s++)
				{
					if(cnt==1 && l<=k) dp[i][k][s]=0;
					else if(cnt==2 && (k<l || l<=s)) dp[i][k][s]=0;
					else if(cnt==3 && s<l) dp[i][k][s]=0;
				}
			}
		}
		
		if(i==n) break;
		
		for(int j=0;j<i;j++)
		{
			for(int k=0;k<=max(0ll,j-1);k++)
			{
				dp[i+1][j][k]=(dp[i+1][j][k]+dp[i][j][k])%mod;
				dp[i+1][i][k]=(dp[i+1][i][k]+dp[i][j][k])%mod;
				dp[i+1][i][j]=(dp[i+1][i][j]+dp[i][j][k])%mod;
			}
		}
	}
	
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<=max(0ll,i-1);j++)
		{
			ans=(ans+dp[n][i][j])%mod;
		}
	}
	
	cout<<(ans*3)%mod;
	
	return 0;
}

014【HAOI 2011】Problem c

这题确实NB,难想的一批 用sum[i]表示i及i以后的已经安排好的人的个数,如果这个人数都已经大于i及以后的总人数了 直接GG

否则的话设dp[i][j]表示从后往前已经到了第i个编号已经安排好了j个人(不算必须安排的人数)然后枚举要在i-1的位置放上多少个人(这里算上必须放的,后边减去),组合数计算一下有多少种方案即可

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#define int long long

using namespace std;

const int maxn=310;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int t,n,m,M;
int sum[maxn];
int must[maxn];
int c[maxn][maxn];
int p[maxn],q[maxn];
int dp[maxn][maxn];

void init()
{
	memset(c,0,sizeof(c));
	memset(dp,0,sizeof(dp));
	memset(sum,0,sizeof(sum));
	memset(must,0,sizeof(must));
	
	c[0][0]=1;
	
	for(int i=1;i<=n;i++)
	{
		c[i][0]=1;
		for(int j=1;j<=i;j++)
		{
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%M;
		}
	}
}

signed main()
{
	t=read();
	
	while(t--)
	{
		n=read();
		m=read();
		M=read();
		bool check=false;
		
		init();
		
		for(int i=1;i<=m;i++)
		{
			p[i]=read(),q[i]=read();
			must[q[i]]++;
		}
		
		for(int i=n;i;i--)
		{
			sum[i]=sum[i+1]+must[i];
			if(sum[i]>n-i+1)
			{
				check=true;
				break;
			}
		}
		
		if(check) { cout<<"NO"<<'\n';continue; }
		
		dp[n][0]=1;
		
		for(int i=n;i>=1;i--)
		{
			for(int j=0;j<=n;j++)
			{
				if(!dp[i][j]) continue;
				for(int k=must[i];k<=n-i+1-j-sum[i+1];k++)
					dp[i-1][j+k-must[i]]=(dp[i-1][j+k-must[i]]+dp[i][j]*c[n-m-j][k-must[i]])%M;
			}
		}
		cout<<"YES"<<" "<<dp[0][n-m]<<'\n';
	}
	
	return 0;
}

015 多米诺骨牌

好题,先用一个前缀和统计出来总共有多少个点,然后背包

dp[i][j]表示前i列,上面一行的点数字和是j最小交换次数,总共的点数可以预处理,下面的点数就用总共的点数减去上面的点数

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<climits>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>

using namespace std;

const int maxn=1010;

const int maxm=6010;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,sum;
int a[maxn];
int b[maxn];
int dp[maxn][maxm];

int main()
{
	n=read();
	
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		b[i]=read();
		sum+=a[i]+b[i];
	}
	
	memset(dp,0x3f3f3f,sizeof(dp));
	
	dp[1][a[1]]=0,dp[1][b[1]]=1;

	for(int i=2;i<=n;i++)
	{
		for(int j=0;j<=maxm;j++)
		{
			if(j-a[i]>=0) dp[i][j]=min(dp[i][j],dp[i-1][j-a[i]]);
			if(j-b[i]>=0) dp[i][j]=min(dp[i][j],dp[i-1][j-b[i]]+1);
		}
	}
	
	int cnt=INT_MAX,val=INT_MAX;
	
	for(int i=0;i<=sum;i++)
	{
		if(dp[n][i]!=1061109567)
		{
			if(abs(i-(sum-i))<cnt)
			{
				cnt=abs(i-(sum-i));
				val=dp[n][i];
			}
			else if(abs(i-(sum-i))==cnt)
			{
				val=min(val,dp[n][i]);
			}
		}
	}
	
	cout<<val;
	
	return 0;
}

016 【APIO/CTSC 2007】数据备份

其实这题还是双倍经验 先进行一个差分,然后就变成了 \(n-1\) 个数,选 \(k\) 个使总和最大(不能选相邻的)

朴素DP:dp[i]表示前i个的最大价值总和,\(dp[i]=min(dp[i-1],dp[i-2]+a[i])\),之后发现可以WQS二分优化

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<climits>
#define int long long

using namespace std;

const int maxn=100010;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int f[maxn];
int dp[maxn];
int dis[maxn];
int mid_ans,ans;
int n,k,s[maxn];

bool check(int x)
{
	memset(dp,0x3f3f3f,sizeof(dp));

	for(int i=2;i<=n;i++)
	{
		dis[i]=s[i]-s[i-1]+x;
	}
	
	dp[0]=0,dp[1]=0;
	
	for(int i=2;i<=n;i++)
	{
		dp[i]=dp[i-1],f[i]=f[i-1];
		if(dp[i]>dp[i-2]+dis[i])
		{
			dp[i]=dp[i-2]+dis[i],f[i]=f[i-2]+1;
		}
		else if(dp[i]==dp[i-2]+dis[i])
		{
			f[i]=min(f[i],f[i-2]+1);
		}
	}
	
	ans=dp[n];
	
	return f[n]<=k;
}

signed main()
{
	n=read(),k=read();
	
	for(int i=1;i<=n;i++) s[i]=read();
	
	int l=-1e9,r=0;
	
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid))
		{
			r=mid-1;
			mid_ans=mid;
		}
		else l=mid+1;
	}
	
	check(mid_ans);
	
	printf("%lld",ans-mid_ans*k);
	
	return 0;
}

017 April Fools' Problem

其实这题也是双倍经验 而且其实这题都不是DP,只是被我拿来凑数的

既然可以WQS二分,就可以看作DP 反悔贪心+WQS二分

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<queue>
#define int long long

using namespace std;

const int maxn=500010;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,k,ans;
int a[maxn];
int b[maxn];
int mid_ans;

priority_queue <int> p;
priority_queue <int,vector<int>,greater<int> > q;

bool check(int mid)
{
	while(!q.empty()) q.pop();
	while(!p.empty()) p.pop();
	int res=0,cnt=0;
	for(int i=1;i<=n;i++)
	{
		q.push(a[i]-mid);
		if(p.size() && ((-p.top()+b[i]<0 && -p.top()<q.top()) || (b[i]+q.top()>=0 && p.top()>b[i])))
		{
			res-=p.top();
			res+=b[i];
			p.pop(),p.push(b[i]);
		}
		else if(q.top()+b[i]<0)
		{
			res+=b[i]+q.top();cnt++;
			q.pop(),p.push(b[i]);
		}
	}
	ans=res;
	return cnt<=k;
}

signed main()
{ 
	n=read();k=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=n;i++) b[i]=read();
	
	int l=-1e10,r=1e10;
	
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid))
		{
			l=mid+1;
			mid_ans=mid;
		}
		else r=mid-1;
	}
	
	check(mid_ans);
	
	printf("%lld",ans+mid_ans*k);
	
	return 0;
}

018 【APIO 2014】序列分割

斜率优化,可以发现结果与切的顺序无关,设 \(dp[i][j]\) 为前 \(i\) 个数分为 \(j\) 块的最大的分,转移方程

\(dp[i][j]=d[k][j-1]+sum[j]\times (sum[i]-sum[j])\),令g[i]为上一次切( \(j-1\) 块)的最大价值(将第二维滚掉)就可以斜率优化了

\(dp[i]=g[k]+sum[j]\times (sum[i]-sum[j])\)

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>

using namespace std;

const int maxn=100010;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int n,k,head,tail;
long long pre[maxn][200];
long long q[maxn],g[maxn];
long long dp[maxn],sum[maxn];

inline long long X(int i)
{
	return sum[i];
}

inline long long Y(int i)
{
	return g[i]-sum[i]*sum[i];
}

inline double slope(int a,int b)
{
	if(X(a)==X(b)) return -1e18;
	return 1.0*(Y(a)-Y(b))/(X(b)-X(a));
}

int main()
{
	n=read(),k=read();
	
	for(int i=1;i<=n;i++)
	{
		sum[i]=sum[i-1]+read();
	}
	
	for(int j=1;j<=k;j++)
	{
		head=tail=1,q[head]=0;
		for(int i=1;i<=n;i++) g[i]=dp[i];
		for(int i=1;i<=n;i++)
		{
			while(head<tail && slope(q[head],q[head+1])<=sum[i]) head++;
			dp[i]=g[q[head]]+sum[q[head]]*(sum[i]-sum[q[head]]);
			pre[i][j]=q[head];
			while(head<tail && slope(q[tail-1],q[tail])>=slope(q[tail],i)) tail--;
			q[++tail]=i;
		}
	}
	
	cout<<dp[n]<<endl;
	for(int ans=n,i=k;i>=1;i--)
	{
		ans=pre[ans][i];
		cout<<ans<<' ';
	}
	
	return 0;
}

019 【ZJOI 2007】仓库建设

\(dp[i]\) 为第 \(i\) 个位置建设仓库的最小费用 \(dp[i]=dp[j]+\sum_{k=j+1}^{i}(x_i-x_k)\times p_k+c_i\)

\(sum\)\(p\) 的前缀和,\(sumc\)\(x_k\times p_k\)

\(dp[i]=dp[j]+x_i\times (sum_i-sum_j)-(sumc_i-sumc_j)+c_i\)

斜率优化即可

AC code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<climits>
#include<algorithm>

using namespace std;

const int maxn=1e6+5;

inline int read()
{
	int w=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

long long dp[maxn];
int q[maxn],head=1,tail=1;
int n,x[maxn],p[maxn],c[maxn];
long long sum[maxn],sumc[maxn];

double X(int i) {return (double)sum[i];}
double Y(int i) {return (double)dp[i]+sumc[i];}
long double slope(int a,int b)
{
	if(X(a)==X(b)) return LLONG_MAX;
	return 1.0*(Y(a)-Y(b))/(X(a)-X(b));
}

int main()
{
	n=read();
	
	for(int i=1;i<=n;i++)
	{
		x[i]=read();
		p[i]=read();
		c[i]=read();
		sum[i]=sum[i-1]+p[i];
		sumc[i]=sumc[i-1]+x[i]*p[i];
	}
	
	for(int i=1;i<=n;i++)
	{
		while(head<tail && x[i]>slope(q[head],q[head+1])) head++;
		dp[i]=dp[q[head]]+x[i]*(sum[i]-sum[q[head]])-(sumc[i]-sumc[q[head]])+c[i];
		while(head<tail && slope(q[tail],q[tail-1])>slope(q[tail],i)) tail--;
		q[++tail]=i;
	}
	
	long long ans=LLONG_MAX;
	
	for(int i=n;i>=1;i--) 
	{
		ans=min(ans,dp[i]);
		if(p[i]) break ;
	}
	
	printf("%lld",ans);
	
	return 0;
}

UPD 2023.7.15

AFO八九个月,才想起来自己还有这么大的坑没补上

虽然但是暂时鸽了,2024一定补到50+道DP

posted @ 2022-10-16 14:58  NinT_W  阅读(66)  评论(4编辑  收藏  举报