AtCoder Beginner Contest 269 G(DP)

题目链接
G - Reversible Cards 2(DP)

题意
N N N张卡片,正面写有数字 A i A_i Ai,背面写有数字 B i B_i Bi ∑ ( A i + B i ) = M \sum(A_i+B_i)=M (Ai+Bi)=M。对于 k ∈ [ 0 , M ] k \in [0,M] k[0,M],求最少翻转多少张卡片,使得所有卡片可见数字之和为 k k k,不存在输出 − 1 -1 1

分析
f [ i ] f[i] f[i]表示最少翻转 f [ i ] f[i] f[i]次可以使得可见数字之和为 i i i,根据状态表示有 f [ S ] = 0 f[S]=0 f[S]=0 S = ∑ A i S=\sum A_i S=Ai.
考虑翻转一张卡片带来的影响,翻转第 i i i张卡片可见数字之和增加 B i − A i B_i-A_i BiAi.接下来对问题进行抽象,将每张卡片看做一个物品,体积为 B i − A i B_i-A_i BiAi,花费为 1 1 1,求最少选择多少个物品可以使花费最少,每个物品只能选一次,这样就转化为01背包问题,容易想到 O ( N M ) O(NM) O(NM) 的做法。

const int N=2e5+10;
int a[N],b[N],f[N],g[N];
int n,m,sum;

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++) sum+=a[i],a[i]=b[i]-a[i];
	f[sum]=0;
	for(int i=1;i<=n;i++)
	{
		int x=a[i];
		for(int j=0;j<=m;j++) g[j]=f[j];
		for(int j=0;j<=m;j++)
		{
			if(x+j>=0&&x+j<=m)
			{
				g[x+j]=min(g[x+j],f[j]+1);
			}
		}
		for(int j=0;j<=m;j++) f[j]=g[j];
	}
	for(int i=0;i<=m;i++)
	{
		if(f[i]==0x3f3f3f3f) cout<<-1<<endl;
		else cout<<f[i]<<endl;
	}
	return 0;
}

O ( N M ) O(NM) O(NM)的复杂度不能通过本题,需要进行优化。由于 ∑ ∣ B i − A i ∣ ≤ ∑ ( A i + B i ) = M \sum|B_i-A_i| \leq \sum(A_i+B_i)=M BiAi(Ai+Bi)=M,可知最多有 M \sqrt M M 种不同的 ∣ B i − A i ∣ |B_i-A_i| BiAi B i − A i B_i-A_i BiAi 最多有 2 M 2 \sqrt M 2M 种取值。通过上面的分析,可以对具有相同体积的物品进行合并,这样问题从01背包转化为多重背包。多重背包可以通过二进制拆分进行优化,时间复杂度为 O ( M M ) O(M \sqrt M) O(MM ),可以通过本题。虽然对于单个物品来说,复杂度还有一个 l o g log log,但通过均摊最终的复杂度不带 l o g log log。可以从直观上理解,物品种类越多时,单个物品的个数就越少, l o g log log接近一个常数,当物品种类很少时,时间复杂度变为 M l o g ( M ) Mlog(M) Mlog(M),运行速度相对更快。

AC代码

const int N=2e5+10;
int a[N],b[N],f[N],cnt[N<<1];
int n,m,sum;

void solve(int x,int v)
{
	if(x>0)
	{
		for(int i=m-x;i>=0;i--)
		{
			f[i+x]=min(f[i+x],f[i]+v);
		}
	}
	else
	{
		for(int i=-x;i<=m;i++)
		{
			f[i+x]=min(f[i+x],f[i]+v);
		}
	}
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++)
	{
		sum+=a[i];
		cnt[b[i]-a[i]+m]++;
	}
	f[sum]=0;
	for(int i=-m;i<=m;i++)
	{
		if(cnt[i+m]&&i)
		{
			int c=cnt[i+m];
			int j=1;
			while(c>=j)
			{
				c-=j;
				solve(i*j,j);
				j<<=1;
			}
			if(c) solve(i*c,c);
		}
	}
	for(int i=0;i<=m;i++)
	{
		if(f[i]==0x3f3f3f3f) cout<<-1<<endl;
		else cout<<f[i]<<endl;
	}
	return 0;
}

注意
使用一维数组表示状态,要注意状态转移顺序,一个状态只能被同一个物品更新一次。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值