[WC2022] 杂题选讲-邓明扬

stars

题目描述

一颗星星可以抽象成 \(k\) 维空间中的一个整点。称若干星星构成的集合 \(s\) 是奇妙的,当且仅当存在 \(k\) 维空间中的整点 \(P\)\(P\)\(s\) 中的每颗星星至少有一维坐标相同。

有一个长度为 \(n\) 的星星序列 \(A\) ,请你求出所有奇妙子区间的个数之和。

\(1\leq n\leq 10^5,1\leq k\leq 5\)

解法

首先考虑如何判定序列 \(s\) 是奇妙的,由于 \(k\) 很小我们可以直接使用枚举法。由于每个点都需要被覆盖,所以我们可以考虑枚举如何解决掉序列 \(s\) 的第一个点,其实就是枚举 \(P\) 的哪个位置用于解决它,然后往后推看有多少点顺带被解决了,遇到下一个不能解决的点再枚举 \(P\) 的一个位置。

不难发现只需要枚举长度为 \(k\) 的所有排列就可以判定合法性,但是暴力判定还是不可行,我们考虑用 \(dp\) 来优化这个过程,由于排列非常小我们可以直接塞状态里面。设 \(dp[i][s]\) 表示后 \(i\) 个点用位置排列 \(s\) 的最远延伸距离(其中 \(s\) 的单个元素被邓老师形象地称为锦囊),考虑如何转移。

写出转移需要强大的观察能力,这里邓老师观察出了问题之间的相似性。我们考虑 \(dp[i][s]\)\(dp[i+1][s']\)(其中 \(s'\) 表示 \(s\) 去掉第一个锦囊之后的位置排列,设其为 \(x_0\)),只是对于 \(dp[i+1][s']\) 第一个需要新增锦囊但是可以被 \(x_0\) 解决的位置,是不需要再新增锦囊的。那么我们可以让 \(x_0\) 去解决它,所以我们把 \(x_0\) 插入到当前最后一个锦囊的下一个位置就得到了 \(i+1\) 的等效子问题,也就是对于以后的影响都等效地传递下去了。

实现小细节:\(dp[i][s]\) 开成一个 \(k\) 维结构体,每一维记录使用前 \(i\) 个锦囊获得的最远延伸距离。

总结

寻找子问题的关键:观察问题之间的相似性。

如果某个元素对于以后的影响是长远的,但又只能考虑一小步转移,那么寻找当前问题的等效子问题,就可以把这个影响传递下去,完成转移。

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
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,cnt,a[M][5],b[M];long long ans=1;
struct node
{
	int p[5];
	int &operator[](int x) {return p[x];}
	int Hash()
	{
		int hs=0;
		for(int i=0;i<m;i++) hs=hs*5+p[i];
		return hs;
	}
}w,dp[2][125];
signed main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
		for(int j=0;j<m;j++) a[i][j]=read();
	for(int i=0;i<m;i++) w.p[i]=i;
	do {b[w.Hash()]=++cnt;}
	while(next_permutation(w.p,w.p+m));
	for(int i=0;i<=120;i++)
		for(int j=0;j<m;j++) dp[n&1][i][j]=n+1;
	for(int i=n-1;i>=1;i--)
	{
		int mx=0,o=(i+1)&1;cnt=0;
		for(int j=0;j<m;j++) w.p[j]=j;
		do
		{
			cnt++;
			if(a[i][w[0]]==a[i+1][w[0]])
			{
				dp[i&1][cnt]=dp[o][cnt];
				mx=max(mx,dp[o][cnt][m-1]);
				continue;
			}
			node t;int p=m;
			for(int j=0;j<m;j++) t[j]=w[(j+1)%m];
			node nw=dp[o][b[t.Hash()]];
			for(int j=0;j<m-1;j++)
			{
				if(a[i][w[0]]==a[nw[j]][w[0]])
				{
					for(int k=m-1;k>=j+1;k--)
						t[k]=t[k-1];
					t[j+1]=w[0];p=j+1;break;
				}
			}
			dp[i&1][cnt]=dp[o][b[t.Hash()]];
			for(int j=p-1;j;j--)
				dp[i&1][cnt][j]=dp[i&1][cnt][j-1];
			dp[i&1][cnt][0]=i+1;
			mx=max(mx,dp[i&1][cnt][m-1]);
		}while(next_permutation(w.p,w.p+m));
		ans+=mx-i;
	}
	printf("%lld\n",ans);
}

Od deski do deski

题目描述

\(n\) 棵树,每棵树可能是 \(m\) 中之一。小 \(C\) 每天可以选择连续的一段树砍掉,要求这一段的长度至少是 \(2\),并且第一棵与最后一棵的种类相同。问有多少种初始局面,使得存在一种方式把树砍完,答案对 \(10^9+7\) 取模。

\(n\leq 3000,m\leq 10^9\)

解法

首先还是考虑如何判定,显然的思路是设 \(dp[i]\) 表示砍完前 \(i\) 棵树是否可行,那么转移就枚举 \(1\leq j<i\),当 \(dp[j-1]=1\and a[i]=a[j]\) 都成立的时候 \(dp[i]=1\)

考虑方案的区分其实就是靠 \(a\),所以计数的方法是考虑当前位置可以填上多少个不同的 \(a[i]\),这取决于满足 \(dp[j-1]=1\) 不同的 \(a[j]\) 数量。

那么我们尝试在状态中记录这个数量然后 \(dp\),还要保证填入 \(a[i]\) 之后这个数量可以被更新。设 \(dp[i][j][0/1]\) 表示考虑前 \(i\) 个位置,其中有 \(j\) 个不同的满足条件的 \(a\)\(dp[i]=0/1\) 的不同初始序列总数,我们枚举上一个状态 \(dp[i-1][j][k]\),那么转移:

\[dp[i][j][1]\leftarrow dp[i-1][j][k]\times j \]

\[dp[i][j+k][0]\leftarrow dp[i-1][j][k]\times(m-j) \]

时间复杂度 \(O(n^2)\)

#include <cstdio>
#define int long long
const int M = 3005;
const int MOD = 1e9+7;
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,ans,dp[M][M][2];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	n=read();m=read();
	dp[0][0][1]=1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=i;j++)
			for(int k=0;k<2;k++)
			{
				add(dp[i][j][1],dp[i-1][j][k]*j%MOD);
				add(dp[i][j+k][0],dp[i-1][j][k]*(m-j)%MOD);
			}
	for(int j=0;j<=n;j++)
		add(ans,dp[n][j][1]);
	printf("%lld\n",ans);
}

Robbery

题目描述

\(n\) 种物品,第 \(i\) 种质量为 \(i\),价格为 \(a_i\),每种物品的数量无限。给定 \(k,w\),请你选择 \(k\) 个物品,满足质量总和是 \(w\),价值之和最大,请求出最大的价值之和。

\(n\leq 1000,k\leq 10^6,k\leq w\leq kn,1\leq a_i\leq 10^9\)

解法

由于物品的质量取值范围是 \([1,n]\),那么有一个经典的结论:存在方法把最优选取的物品分成两部分,两部分的差值小于等于 \(n\),这个结论为我们提供了分治的可能。

\(dp[k][w]\) 表示 \(k\) 个物品,权值总和为 \(w\) 的价值最大值,可以写出暴力的转移:

\[dp[k][w]=\begin{cases} dp[k-1][w-i]+a_i&k\bmod2=1,i\in[1,n]\\ dp[\frac{k}{2}][\frac{w}{2}-i]+dp[\frac{k}{2}][\frac{w+1}{2}+i] & k\bmod 2=0,i\in[-\frac{n}{2},\frac{n}{2}] \end{cases} \]

我们考虑状态总数,因为 \(\frac{w}{2}\) 的操作,所以每一层的个数大概是 \(n+\frac{n}{2}+\frac{n}{4}....=O(n)\),一共有 \(O(\log k)\) 层。转移的复杂度是 \(O(n)\) 的,所以总复杂度 \(O(n^2\log k)\)

小细节:别像我一样乱写 \(\tt map\) 超时了,可以预处理出每一层的标准长度,那么状态一定是在标准长度上下浮动的,所以用和标准长度的差值加上一个常量开数组就可以了。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 1005;
const int N = 1000005;
#define int long long 
const int inf = 1e18;
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,k,w,a[M],ind[N],tot[N],dp[50][M<<3];
int f(int k,int w)
{
	if(w<k || w>n*k) return -inf;
	if(!k) return 0;
	int &t=dp[ind[k]][w-tot[k]+2500];
	if(t!=-1) return t;
	if(k%2)
	{
		for(int i=1;i<=n;i++)
			t=max(t,f(k-1,w-i)+a[i]);
	}
	else
	{
		for(int i=-n/2;i<=n/2;i++)
			t=max(t,f(k/2,w/2-i)+f(k/2,(w+1)/2+i));
	}
	return t;
}
signed main()
{c++
	n=read();k=read();w=read();tot[k]=w;
	memset(dp,-1,sizeof dp);
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=k,cnt=0;i>=1;)
	{
		if(i&1) ind[i]=++cnt,tot[i-1]=tot[i],i--;
		else ind[i]=++cnt,tot[i/2]=tot[i]/2,i>>=1;
	}
	printf("%lld\n",f(k,w));
}

Poborcy podatkowi

题目描述

有一棵 \(n\) 个点的树,边有边权(可能为负数),请你找出一些不交的长度为 \(4\) 的路径,使得权值之和最大,输出权值之和的最大值。

\(n\leq 2\cdot 10^5\)

解法

可以直接树形 \(dp\),设 \(dp[u][0/1/2/3]\) 表示 \(u\) 为根留给父亲的链长度是 \(0/1/2/3\) 的最大权值。合并的儿子的时候考虑必须要有偶数个 \(dp[v][1]\)(长度为 \(2\) 的链),\(dp[v][0],dp[v][2]\) 的数量必须相等(凑出长度为 \(4\) 的链),\(dp[v][3]\) 可以直接并上来。

根据上面的讨论,我们设计 \(g[i][j]\) 表示长度为 \(2\) 的链奇偶性是 \(i\),长度为 \(1\) 的链和长度为 \(3\) 的链之差是 \(j\),那么转移就类似于做一个背包,根据经典结论我们可以把儿子序列 \(\tt random\_shuffle\) 一下,第二维就可以限制在 \(\sqrt n\) 了:

当然上面的东西我是看不懂的,时间复杂度 \(O(n\sqrt n)\)

#include <cstdio>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <ctime>
using namespace std;
const int M = 200005;
const int N = 450;
#define int long long
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,dp[M][4];
struct edge{int v,c;};vector<edge> g[M];
void upd(int &x,int y) {x=max(x,y);}
void dfs(int u,int fa)
{
	int pl[2][2][N*2+10]={};
	memset(pl,-0x3f,sizeof pl);
	int cnt=0,w=0,*f[2][2]=
	{pl[0][0]+N+5,pl[0][1]+N+5,
	pl[1][0]+N+5,pl[1][1]+N+5};
	//
	random_shuffle(g[u].begin(),g[u].end());
	for(auto x:g[u]) if(x.v!=fa) dfs(x.v,u);
	//
	int mi=f[0][0][0];f[0][0][0]=0;
	for(auto x:g[u]) if(x.v^fa)
	{
		int v=x.v,c=x.c;
		for(int i=0;i<2;i++)
		for(int j=-cnt;j<=cnt;j++)
		{
			int tmp=f[w][i][j];if(tmp<=mi) continue;
			upd(f[!w][i][j],tmp+dp[v][0]);//do nothing
			upd(f[!w][i][j],tmp+c+dp[v][3]);//3+(1)
			upd(f[!w][!i][j],tmp+c+dp[v][1]);//2+2
			if(j>-N) upd(f[!w][i][j-1],tmp+c+dp[v][2]);
			if(j<N) upd(f[!w][i][j+1],tmp+c+dp[v][0]);
		}
		w^=1;if(cnt<N) cnt++;
	}
	dp[u][0]=f[w][0][0];
	dp[u][1]=f[w][0][1];
	dp[u][2]=f[w][1][0];
	dp[u][3]=f[w][0][-1];
}
signed main()
{
	srand(time(0));n=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read(),c=read();
		g[u].push_back(edge{v,c});
		g[v].push_back(edge{u,c});
	}
	dfs(1,0);
	printf("%lld\n",dp[1][0]);
}
posted @ 2022-01-27 16:32  C202044zxy  阅读(697)  评论(1编辑  收藏  举报