DP · 真题


下面是几道CSP/NOIP真题(如题,都是DP)

[NOIP2017 普及组] 棋盘

[NOIP2014 提高组] 飞扬的小鸟

[NOIP2015 提高组] 子串

[NOIP2017 普及组] 跳房子

[NOIP2018 普及组] 摆渡车

(大概按难易程度排了一下吧).


棋盘

看题像极了DP,但数据范围没有那么大,\(so\) 记忆化搜索似乎能过

用一个数组 \(ans[i][j][dir](0\leq dir<4)\) 表示在 \((i,j)\) 这个节点上,上一步从 \(dir\) 这个方向来的硬币花费最小。( \(dir\) 具体表示哪个方向并不用在意).

如果 走到当前这个点的花费 \(\geq ans[x][y][dir]\) 那就不用再搜下去了.

(个人认为这个方法十分的玄学,但它过了).

放一个 \(dfs\) 的代码吧:

const int Ma=0x3f3f3f3f;
int n,m;
int mp[105][105],ans[105][105][4];
bool v[105][105];
int dx[4]={0,0,-1,1},dy[4]={1,-1,0,0};

void dfs(int xx,int yy,int coin,bool is,int dir){
	if(ans[xx][yy][dir]<=coin)	return;
	ans[xx][yy][dir]=coin;
	if(xx==n && yy==n)	return;
	
	for(int i=0;i<4;++i){
		int x=dx[i]+xx,y=dy[i]+yy;
		if(x>0 && x<=n && y>0 && y<=n && !v[x][y]){
			v[x][y]=1;
			if(mp[x][y]==0){
				if(!is)
					mp[x][y]=mp[xx][yy],dfs(x,y,coin+2,1,i),mp[x][y]=0;
			}
			else{
				if(mp[x][y]==mp[xx][yy])	dfs(x,y,coin,0,i);
				if(mp[x][y]!=mp[xx][yy])	dfs(x,y,coin+1,0,i);
			}
			v[x][y]=0;
		}
	}
}

飞扬的小鸟

一看就是DP,但是细节稍稍有亿点点多.

用一个二维数组 \(f[i][j]\) 表示在横坐标为 \(i\) ,高度为 \(j\) 的位置上的最小点击次数.

再用当前这个状态去更新横坐标为 \(i+1\) 的可到达高度的 \(f\) 数组.

一个没有过的(\(T\) 掉了)\(dp\) 核心:

for(int i=0;i=hig[i])){
				f[i][j]=Maxn;
				continue;
			}
			if(j-dow[i]>low[i+1] && j-dow[i]<(hig[i+1]==m?m+1:hig[i+1]))
				f[i+1][j-dow[i]]=min(f[i+1][j-dow[i]],f[i][j]);
			
			int k;
			if(hig[i+1]==m){
				int tmp=Maxn;
				for(k=1;tmp!=m;++k)
					tmp=min(m,j+fl[i]*k),f[i+1][tmp]=min(f[i+1][tmp],f[i][j]+k);
			}
			else{
				for(int k=1;k

子串

用数组 \(f[i][j][k]\) 表示 \(a\) 字符串前 \(i\) 个字符构成的字符串与 \(b\) 字符串前 \(j\) 个字符构成的字符串,划分 \(k\) 次的匹配方案数.

\[f[i][j][k]=f[i-1][j-1][k](a[i]\neq b[j]) \]

\[f[i][j][k]=f[i-1][j][k]+f[i-1][j-1][k-1](a[i]=b[j],a[i-1]\neq b[j-1]) \]

\[... \]

\(dp\) 核心:

f[0][0]=1;
for(int i=1;i<=n;++i)
	for(int j=m;j>=1;--j)
		for(int q=k;q>=1;--q){
			sum[j][q]=(a[i-1]==b[j-1]?sum[j-1][q]+f[j-1][q-1]:0)%mod;
			f[j][q]=(f[j][q]+sum[j][q])%mod;
		}

跳房子

数据范围这么大,就用二分枚举使用的金币个数,最关键的就是 \(check\) 函数了

这肯定就是个DP了 (废话,出现在这篇博客里的不都是吗)

\(f[i]\) 表示第跳到第 \(i\) 个点时,最大的分数

\[f[i]=\max_{i-(d+k)\leq j\leq i-max(d-k,1)}(f[j])+f[i] \]

直接找最大值有点费时,用一个单调队列优化一下

\(check\) 函数:

bool ch(int q){
	int l=max(1ll,d-q),r=d+q;
	deque mm;
	int j=0;
	for(int i=1;i<=n;++i){
		while(j=l){
			if(!mm.size())	mm.push_back(j);
			else{
				while(mm.size() && f[j]>=f[mm.back()])
					mm.pop_back();
				mm.push_back(j);
			}
			++j;
		}
		while(mm.size() && x[i]-x[mm.fr]>r)	mm.pop_front();
		if(mm.size())	f[i]=f[mm.fr]+s[i];
		else	f[i]=-999999999999;
		if(f[i]>=k)	return 1;
	}
	return 0;
}

一定! 一定! 记得 define int ll

(否则最后一个点过不去)


摆渡车

这样把他抽象化一下:
rphDn.png

\(f[i]\) 表示最后一段的右边界为 \(i\) 的方案的最小等车时间.

\[f[i]=\min_{0\leq j\leq i-m}(\sum_{j

注意\(i\) 最大要取到 \((\max t[k])+m\)

这样转移的复杂度远大于 \(O(t^2)\) ,这不就 \(TLE\) 了嘛

拆分一下:

\[\sum_{j

\(\sum i\)\(j\)\(i-m\) 之间新来的等车人数

\(\sum t_k\) 为这段时间内每一个人的到达时间之和

这两个和都可以用 前缀和 预处理

这样的时间复杂度到了 \(O(t^2)\) ,可惜还是会 \(T\)

\(i\leq i-2m\) 时可以跑至少两个来回,这样一定会更优,所以 \(j\) 只需要从 \(i-2m\) 枚举到 \(i-m\),这样就 \(A\)

\(dp\) 核心:

int ans=1e9;
for(int i=m;i<=mt+m;++i){
	if(cnt[i]==cnt[i-m]){
		f[i]=f[i-m];
		continue;
	}
	for(int j=max(0,i-m*2+1);j<=i-m;++j)
		f[i]=min(f[i],f[j]+sum[i]-sum[j]-cnt[j]*(i-j));
}