dp小练习题解(长期更新)

下学期学校开DS和算法的课了,年前复习一下自己高中时候做过的一些dp好题

自从退出ACM之后感觉也蛮久没敲了,顺便也练一练码力

不过回归发现bzoj没了,很多题只能敲一敲,不知道能在哪儿评测,也不知道对不对了((

随缘更新,在2月之前应该能更完

bzoj2287

题意:给出n个物品装容量为M的背包,对于每一个物品i,问当i缺失的时候用剩下i-1个物品装满背包的方案数(n,m<=2e3)

首先考虑一个整的01背包,用傻瓜方法得出f[j]为背包体积为j的时候装满背包的方案书。为然后对于单个物品i,定义g[j]为i物品缺失的时候装满j体积的方案数,直接求得这个结果是有些复杂的(因为需要重新做一次背包,如此的时间复杂度自然是1s承受不了的),因此我们选择容斥,用f[j]减去必定选择此物品i的方案数,此状态可以直接由g[j-w[i]]转移过来,最后输出g[m]即可得出结果

#include<cstdio>
#include<algorithm>
#include<cmath>
#include<iostream>
#define eps (1e-6)
#define PI (acos(-1))
using std::cin;using std::cerr;
using std::cout;using std::endl;
const int MOD=10;
const int MAXN=2e3+5;
int f[MAXN],g[MAXN],w[MAXN];
inline int read()
{
	bool flag(0);int x(0);char ch;
	while(!isdigit(ch=getchar())){(ch=='-')&&(flag=1);}
	for(;isdigit(ch);x=(x<<1)+(x<<3)+(ch^48),ch=getchar());
	return flag?-x:x;
}
int main()
{
	int n(read()),m(read());
	for(int i=1;i<=n;i++)
	{
		w[i]=read();
	}
	f[0]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=m;j>=w[i];j--)
		{
			f[j]=(f[j]+f[j-w[i]])%10;
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=m;j++)
		{
			if(j<w[i])
			{
				g[j]=f[j];
				continue;
			} 
			else g[j]=(f[j]-g[j-w[i]]+10)%10;
		}
		for(int j=1;j<=m;j++)
		{
			printf("%d",g[j]);
			j==m?puts(""):printf(" ");
		}
	}
	return 0;
}

poj 3093

题意:给出背包容量和物品体积,问有多少种方案可以使得该背包再也无法装下任何一个物品

将物品顺序排列,则答案可以抽象为,针对每个物品,所有比该物品体积小的全部装进背包,统计用比该物品更大的物品来装满背包,使得该物品无法被装入的方案数,最终答案为这些方案数求和

假设选择的比该物品更大的物品和为sum1,小于该物品的和为sum2该物品体积为vi,背包大小为m,则有:

m-sum2-vi < sum1<=m-sum2

求得这个范围内所有sum1的和

顺序排列之后求后缀和,然后倒序背包求出前K大物品装满背包的方案数,最后把所需答案求和即可得出结果

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>
#include<cstring>
#define LL long long
inline int read()
{
	bool flag(0);int x(0);char ch;
	while(!isdigit(ch=getchar())){(ch=='-')&&(flag=1);}
	for(;isdigit(ch);x=(x<<1)+(x<<3)+(ch^48),ch=getchar());
	return flag?-x:x;
}
const int MAXM=1e3+7;
const int MAXN=55;
int v[MAXN],dp[MAXN][MAXM];
LL sum[MAXN];
bool cmp(const int &a,const int &b)
{
    return a>b;
}
int main()
{
    int T(read());
    for(int cas=1;cas<=T;cas++)
    {
        int ans=0,cnt=1;
        memset(v,0,sizeof(v));
        memset(dp,0,sizeof(dp));
        memset(sum,0,sizeof(sum));
        int n(read()),m(read());
        for(int i=1;i<=n;i++)
        {
        	v[i]=read();
		}
		std::sort(v+1,v+n+1,cmp);
        if(v[n]>m)
        {
            printf("%d 0\n",cas);
            continue ;
        }
        for(int i=n;i>=1;i--)
        {
        	sum[i]=sum[i+1]+v[i];
		}
        for(int i=0;i<=n;i++) 
        {
        	dp[i][0]=1;
		}
        for(int i=1;i<=n;i++)
        {
            for(int j=v[i];j<=m;j++)
            {
            	dp[i][j]=dp[i-1][j]+dp[i-1][j-v[i]];
			}
		}
        for(int i=n;i>=1;i--)
        {
            for(int j=std::max(m-sum[i+1]-v[i]+1,0ll);m>=sum[i+1]&&j<=m-sum[i+1];j++)
            {
            	ans=ans+dp[i-1][j];
			}        	
		}
        printf("%d %d\n",cas,ans);
    }
    return 0;
}
}

bzoj 2794

考虑到询问数量过多,每次全暴力跑背包肯定是遭不住的

条件1的前半句明显是可以靠排序解决的,我们假设离线询问,此时询问的查询随n的增长变成线性关系,我们只考虑对于b的条件进行判断

暴力背包的最大弊端在于状态比较简单,必须在特定的物体中全部跑一次才能得到结果,但由于此问题中我们只需要判断可行性,不需要给出方案/方案数,所以我们可以考虑针对b来进行转移

设f[i]为c[i]和为k的情况下选取集合当中的b[i]最小值的最大值,很显然如果我们能够完成这个状态转移,只要这个值比给定的m+s要大即可

考虑f[i]的转移,很明显对于每一个询问i每次跑一遍容量的背包,单指针加入物品,最后时间复杂度为O(nm+q)

m和m+s也可以形成两个偏序,原理上来说应该也能用CDQ分治来做,不过这里主要写DP做法,就略了

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>
#include<cstring>
#define LL long long
inline int read()
{
	bool flag(0);int x(0);char ch;
	while(!isdigit(ch=getchar())){(ch=='-')&&(flag=1);}
	for(;isdigit(ch);x=(x<<1)+(x<<3)+(ch^48),ch=getchar());
	return flag?-x:x;
}
const int MAXM=1e3+7;
const int MAXN=1e6+5;
const int INF=1e9+7;
class obj
{
	public:
	int a,b,c;
	bool operator<(const obj &z)const 
	{
		return a<z.a;
	}
}a[MAXN];
class Query
{
	public:
	int m,k,s,id;
	bool operator<(const Query &z)const
	{
		return m<z.m;
	}
}q[MAXN];
int f[MAXN];
int ans[MAXN];
int n,m;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		a[i].c=read(),a[i].a=read(),a[i].b=read();
	}
	m=read();
	for(int i=1;i<=m;++i)
	{
		q[i].m=read();q[i].k=read();q[i].s=read();
		q[i].id=i;
	}
	std::sort(a+1,a+n+1);
	std::sort(q+1,q+m+1);
	int idex=1;f[0]=INF;
	for(int i=1;i<=m;++i)
	{
		while(idex<=n&&a[idex].a<=q[i].m)
		{
			for(int k=100000;k>=a[idex].c;--k)
			{
				f[k]=std::max(f[k],std::min(f[k-a[idex].c],a[idex].b));
			}
			idex++;
		}
		ans[q[i].id]=(f[q[i].k]>(q[i].m+q[i].s));
	}
	for(int i=1;i<=m;++i) puts(ans[i]?"TAK":"NIE");
	return 0;
}

bzoj1190

经典分层背包问题,貌似是高二暑假的时候我在班上讲dp题的时候选过的一道(后来发现一半的队友都写过这题了,xs)

很多人给的方法是状压,不过我感觉和状压没啥关系(

明显直接裸的01包是寄了,这么大的背包是开不下的。

发现vi有很明显的二进制数字特征,所以根据vi来做,对于一个数来说确定a和b就相当于确定了这个数,所以直接根据a/b来定义状态

f[i][j]表示一共选择j*(2^i)的背包容量的最大价值,当i相同的情况下基本是入门转移式,不多赘述

考虑层与层之间的转移,把原容量M化为二进制,然后把M的信息加入背包当中,把上述背包的状态定义改为j*(2^i)+(w&((1<< i)-1)容量下的最大价值(新条件保证自己装的容量一定和M有关)

每一次把当前的2^i中分一层到2^(i-1),然后把M的第i位加入即可

第二次(层层之间)的转移方程为

f[i][j]=max(f[i][j],f[i-1][j-k]+f[ i-1][(k<<1)+((w>>i-1)&1)])

预处理将所有V处理为a*2^b形式,其它的按照上述过程就是裸背包

// luogu-judger-enable-o2
#include<cstdio>
#include<cstring>
#include<algorithm>
#define LL long long
namespace stream
{
	void print(LL x)
	{
    	if(x<0)
    	{
        	putchar('-');
        	x=-x;
    	}
    	if(x>9)
    	{
    		print(x/10);
    	}
    	putchar(x%10+'0');
	}	
	void read(int &x)
	{
    	int f=1;x=0;char s=getchar();
    	while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    	while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    	x*=f;
	}
}
const int MAXN=12;
LL dp[35][1005];
int v[105];
int w[105];
int W;
int n;
int main()
{
	while(1)
	{
		stream::read(n);
		stream::read(W);
		if(n==-1&&W==-1)
		{
			break;
		}
		std::memset(dp,0,sizeof(dp));
		for(int i=1;i<=n;i++)
		{
			stream::read(w[i]);
			stream::read(v[i]);
		}
		for(int i=1;i<=n;i++)
		{	
			int s=w[i];
			int cnt=0;
			while(!(s&1))
			{
				cnt++;
				s>>=1;
			}
			for(int j=1001;j>=s;j--)
			{
				dp[cnt][j]=std::max(dp[cnt][j],dp[cnt][j-s]+v[i]);
			}
		}
		int top=0;
		int m=W;
		while(m)
		{
			m>>=1;
			top++;
		}
		top--;
		for(int i=1;i<=top;i++)
		{	
			for(int j=1001;j>=0;j--)
			{
				for(int k=0;k<=j;k++)
				{
					dp[i][j]=std::max(dp[i][j], dp[i][j-k]+dp[i-1][std::min(1001,(k<<1)|((W>>i-1)&1))]);
				}
			}
		}
		stream::print(dp[top][1]);
		std::putchar('\n');
	}
	return 0;
}

bzoj4472

树形dp+贪心(堆求最大值)

定义f[u]为以u为根节点的子树的最大价值,假设次数为t[u]

非常简单的状态转移,针对每个点的次数限制,考虑每一次跑完一个子树,回到该节点之后都需要浪费一次,所以直接先将子树的dp全部跑完塞进堆里取前t[u]-1个即可,中间注意遇到负数马上break,因为不会增加最大收益

再定义sole[u]表示以u为子树的节点的最大价值方案是否唯一,定义0为唯一,1为不为1,考虑子树到当前根节点的转移过程

可以非常明显的发现,只有三种情况可以使得方案不唯一:

1、u的子节点v中存在不唯一方案,即sole[v]=1

2、存在子节点的dp[v],因为此时v可选可不选,造成了多个方案

3、存在一个未被选取的子节点和以被选取的子节点价值相同,意味着可以选择其中一个,造成多个方案

一次dfs转移,算上整个堆排序的过程,复杂度为(nlogn)

// luogu-judger-enable-o2
#include<cstdio>
#include<cstring>
#include<algorithm>
#define LL long long
namespace stream
{
	void print(LL x)
	{
    	if(x<0)
    	{
        	putchar('-');
        	x=-x;
    	}
    	if(x>9)
    	{
    		print(x/10);
    	}
    	putchar(x%10+'0');
	}	
	void read(int &x)
	{
    	int f=1;x=0;char s=getchar();
    	while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    	while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    	x*=f;
	}
}
const int MAXN=12;
LL dp[35][1005];
int v[105];
int w[105];
int W;
int n;
int main()
{
	while(1)
	{
		stream::read(n);
		stream::read(W);
		if(n==-1&&W==-1)
		{
			break;
		}
		std::memset(dp,0,sizeof(dp));
		for(int i=1;i<=n;i++)
		{
			stream::read(w[i]);
			stream::read(v[i]);
		}
		for(int i=1;i<=n;i++)
		{	
			int s=w[i];
			int cnt=0;
			while(!(s&1))
			{
				cnt++;
				s>>=1;
			}
			for(int j=1001;j>=s;j--)
			{
				dp[cnt][j]=std::max(dp[cnt][j],dp[cnt][j-s]+v[i]);
			}
		}
		int top=0;
		int m=W;
		while(m)
		{
			m>>=1;
			top++;
		}
		top--;
		for(int i=1;i<=top;i++)
		{	
			for(int j=1001;j>=0;j--)
			{
				for(int k=0;k<=j;k++)
				{
					dp[i][j]=std::max(dp[i][j], dp[i][j-k]+dp[i-1][std::min(1001,(k<<1)|((W>>i-1)&1))]);
				}
			}
		}
		stream::print(dp[top][1]);
		std::putchar('\n');
	}
	return 0;
}

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值