AtCoder Beginner Contest 221

G.Jumping sequence

题目描述

有一个平面直角坐标系,你初始在 \((0,0)\),目标点是 \((x,y)\),你有 \(n\) 步可以走,每一步步长为 \(d_i\),可以任意选择走上下左右,试构造方案使得能走到终点。

\(n\leq 2000,d_i\leq 1800\)

解法

我们把二维平面逆时针旋转 \(45\) 度,那么终点就变成 \((x-y,x+y)\),向右走的操作变成了 \((d,d)\),向上走的操作变成了 \((-d,d)\) \(...\) 所有的走法都可以被表示成 \((\pm d,\pm d)\)

不难发现两维独立了,所以这从一个二维问题转化成了一个一维问题,再略微的转化一下,我们要找到一个系数数列 \(px_i,py_i\in\{0,1\}\),满足下列条件:

\[\sum px_i\cdot d_i=\frac{sumd+x-y}{2},\sum py_i\cdot d_i=\frac{sumd+x+y}{2} \]

这变成了一个 \(01\) 背包问题,可以用 \(\tt bitset\) 优化到 \(O(\frac{n^2d}{w})\)

总结

给高维问题降维是优化的重要方法,降维的关键是寻找维之间的独立性,这题用到的技巧是二维平面旋转 \(45\) 度。

#include <cstdio>
#include <bitset>
#include <iostream>
using namespace std;
const int M = 2001;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m[2],a[M],x,y,sum;
bitset<3600001> dp[M];string ans;
int Abs(int x) {return x>0?x:-x;}
signed main()
{
	n=read();x=read();y=read();
	m[0]=x-y;m[1]=x+y;
	for(int i=0;i<n;i++) sum+=a[i]=read();
	for(int i=0;i<2;i++)
		if(Abs(m[i])>sum) {puts("No");return 0;}
	for(int i=0;i<2;i++)
	{
		if((m[i]+sum)%2) {puts("No");return 0;}
		else m[i]=(m[i]+sum)/2;
	}
	dp[0][0]=1;
	for(int i=0;i<n;i++) dp[i+1]=dp[i]|(dp[i]<<a[i]);
	if(!dp[n][m[0]] || !dp[n][m[1]])
		{puts("No");return 0;}
	for(int i=n-1;i>=0;i--)
	{
		int x=0;
		for(int j=0;j<2;j++)
			if(!dp[i][m[j]])//must decrease
			{
				m[j]-=a[i];
				x+=(1<<j);
			}
		if(x==0) ans='L'+ans;
		if(x==1) ans='D'+ans;
		if(x==2) ans='U'+ans;
		if(x==3) ans='R'+ans;
	}
	puts("Yes");
	cout<<ans<<endl;
}

H.Count Multiset

题目描述

给定整数 \(n,m\),对于整数 \(k=1,2...n\),分别求出满足下列条件的集合个数:

  • 集合的大小为 \(k\)
  • 集合都是正整数且总和为 \(n\)
  • 一个数 \(x\) 的出现次数至多为 \(m\)

\(m\leq n\leq 5000\)

解法

难以解决的限制是数 \(x\) 的出现次数至多为 \(m\),否则就是一个裸的背包问题了。

解决方案是把集合内元素从大到小排序,然后得到差分数组 \(b_i=a_i-a_{i+1}\),第二个限制转化成:

\[\sum_{i=1}^k b_i\cdot i=n \]

第三个限制转化成不能有连续长为 \(m\) 的一段 \(0\),新增的限制是差分数组的最后一位非 \(0\),设 \(dp[i][j]\) 表示考虑差分数组的前 \(i\) 位总和是 \(j\) 且最后一位非 \(0\) 的方案数。

转移可以从前 \(m\) 个位置而来,所以维护 \(sum[j]\) 记录这一段的 \(dp\) 值之和,就可以 \(O(n^2)\) 转移了。

总结

差分的技巧可以把某个数出现次数的限制转化成 \(0\) 出现次数的限制,更方便讨论。

#include <cstdio>
const int M = 5005;
const int MOD = 998244353;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,dp[M][M],sum[M];
signed main()
{
	n=read();m=read();
	sum[0]=dp[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=i;j<=n;j++)
			dp[i][j]=(sum[j-i]+dp[i][j-i])%MOD;
		for(int j=0;j<=n;j++)
		{
			sum[j]=(sum[j]+dp[i][j])%MOD;
			if(i>=m)
				sum[j]=(sum[j]-dp[i-m][j]+MOD)%MOD;
		}
	}
	for(int i=1;i<=n;i++)
		printf("%d\n",dp[i][n]);
}
posted @ 2021-10-04 20:31  C202044zxy  阅读(533)  评论(0编辑  收藏  举报