简单 dp 题选做

1. USACO 划分选区 redistricting

题目大意

牛分为两类,一类叫荷牛(H),一类叫更牛(G)。两种牛一共占地 \(n\) 块,每块地归一种牛管。定义一个选区是一种牛的当且仅当这个选区中包含的这种牛的地比另外一种牛多。选区最多包含 \(k\) 块地。请你求出选区属于更牛或不属于任何牛的最小可能数量。
数据范围:\(1\leq k\leq n\leq 3\times 10^5\)

分析

考虑 dp。

定义 \(pre_i\) 为前 \(i\) 块地中 H 的数量减去 G 的数量,预处理 \(\Theta(n)\)。实际使用时直接调 \(pre_i>0\) 就能知道前 \(n\) 块地中是荷牛数量多还是更牛数量多。若比较区间 \([i,j]\),则可以直接算 \(pre_i-pre_{i-j}\)。定义 \(dp_i\) 即为前 \(i\) 个块地的答案。

枚举 \(j\in[1,k]\),考虑最后一块选区由几块地组成。所以方程为:

\[dp_i=\min_{1\leq j\leq k}\{dp_{i-j}+[pre_i-pre_{i-j}\leq 0]\} \]

直接枚举是 \(\Theta(nk)\) 的,由于只有与当前位置的前 \(k\) 块地与当前位置的答案有关,考虑单调队列或堆优化即可。复杂度 \(\Theta(n)\)

代码

//priority_queue ver.

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
//#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
//#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
int n,k;
const int MAXN=1e6+5;
char str[MAXN]; int dp[MAXN];
int pre[MAXN];
struct node{
	int pos,dpval;
	bool operator<(const node &x)const{
		if(dpval==x.dpval) return pre[pos]>pre[x.pos];
		else return dpval>x.dpval;
	}
};
priority_queue<node> pq;
signed main()
{
	n=read();k=read(); cin>>str+1;
	for(int i=1;i<=n;i++){
		pre[i]=pre[i-1];
		if(str[i]=='H') pre[i]++;
		else pre[i]--;
	}
	memset(dp,0x7f,sizeof(dp));
	dp[1]=(pre[1]==0);pq.push({0,0});
	for(int i=1;i<=n;i++){
		node tmp=pq.top();
		while(tmp.pos<i-k) pq.pop(),tmp=pq.top();
		dp[i]=min(dp[i],tmp.dpval+(pre[i]<=pre[tmp.pos]));
		pq.push({i,dp[i]});
	}
	write(dp[n]); puts("");
}
//brute force ver.

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
//#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
//#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=1e6+5;
char str[MAXN];
int pre[MAXN],n,k; int dp[MAXN];
signed main()
{
	n=read(),k=read();
	cin>>str+1;
	for(int i=1;i<=n;i++){
		pre[i]=pre[i-1];
		if(str[i]=='H') pre[i]++;
		else pre[i]--;
	}
	memset(dp,0x7f7f,sizeof(dp));
	dp[1]=(pre[1]==0);
	for(int i=1;i<=n;i++)
		for(int j=i-1;j>=i-k&&j>=0;j--){
			dp[i]=min(dp[i],dp[j]+(pre[i]<=pre[j]));
		}
	write(dp[n]); puts("");
}

2. [POI2008] STA-Station

题目大意

给定一棵 \(n\) 个节点的无根树,求一个节点 \(x\)\(s.t.\) 这棵树以 \(x\) 为根时所有节点深度之和最小。
数据范围 \(1\leq n\leq 10^6\)

分析

最简单的做法必然是每个节点为根跑一遍。复杂度 \(\Theta(n^2)\)

我们发现以一个节点为根的答案与其子树大小与父亲节点答案有关。考虑 \(dp\)

直接跑一遍必然只能算一个点的答案。所以跑第二遍。实际上就是换根 dp。

第一遍预处理出一个根的答案,这里以 \(1\) 举例。第二遍跑的时候就可以通过父节点(这里是以 \(1\) 为根的时候的父节点)和预处理的 size 来转移就行力。

考虑节点 \(v\) 和父亲 \(u\),那么有:

\[dp_v=dp_u-2sz_v+n \]

这里需要注意的是只要一个节点 \(x\) 做了根,在原树中为 \(x\) 子树的必然贡献 \(-1\),其余贡献必然 \(+1\),这样的话 \(\Delta=-sz_u+(n-sz_u)=n-2sz_u\)

这样复杂度降到 \(\Theta(n)\)

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=1e6+5;
struct edge{
	int from,to,next;
}e[MAXN*2];
int n,head[MAXN],cntEdge,sz[MAXN],dep[MAXN],dp[MAXN],maxdp,ans;
inline void add(int u,int v){
	cntEdge++;
	e[cntEdge].from=u,e[cntEdge].to=v;
	e[cntEdge].next=head[u],head[u]=cntEdge;
}
inline void dfs1(int u,int fa){
	sz[u]=1; dep[u]=dep[fa]+1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to; if(v==fa) continue;
		dfs1(v,u); sz[u]+=sz[v];
	}
}
inline void dfs2(int u,int fa){
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to; if(v==fa) continue;
		dp[v]=dp[u]-2*sz[v]+n;
		dfs2(v,u);
	}
}
signed main()
{
	n=read();
	for(int i=1;i<=n-1;i++){
		int u=read(),v=read();
		add(u,v); add(v,u);
	}
	dfs1(1,0);
	for(int i=1;i<=n;i++) dp[1]+=dep[i];
	dfs2(1,0);
	for(int i=1;i<=n;i++) if(dp[i]>maxdp) maxdp=dp[i],ans=i;
	write(ans),puts("");
}

3. CF1324F Maximum White Subtree

题目大意

给定一棵 \(n\leq 2\times 10^5\) 个节点的树,对于每一个节点 \(i\),求出包含节点 \(i\) 的一个连通子图,\(s.t.\) 其中白色节点个数 \(-\) 黑色节点个数最大化。

分析

由于树上的连通子图就是子图,直接考虑 dp。

同样的,由于一个节点的答案可能包含了其父节点,所以需要换根。

先钦定 \(1\) 为根,然后先 dp 一遍。

由于我需要计算两种节点数量差,所以这里的一个 trick 是白色节点 \(col\) 标记为 \(1\),黑色节点标记为 \(-1\)

\(dpa_u\) 表示以 \(1\) 为根,\(u\) 的子树内的答案。注意一定包含 \(u\) 本身

\[dpa_u=col_u+\sum_{v\in \text{subtree}(u)}\max(dpa_v,0) \]

这里子树只要答案 \(>0\) 那么计算上就可。

接下来换根计算最终答案 \(dpb\)。考虑一个节点 \(v\) 及其父亲 \(u\)

\[dpb_v=\max(0,dpb_u-\max(0,dpa_v))+dpa_u \]

这里的解释是:对于一个节点 \(v\),我先计算我的父亲对我的答案是否有用,如果有用的话其贡献应该大于 \(0\)
对于我父亲的答案,我先减去与我自身有关的,因为我不再是我父亲的子树,我父亲对我的不能包含原本不是其子树的。
然后我再加上我自己的贡献,就是我自己最终的答案。

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=2e5+5;
struct edge{
	int from,to,next;
}e[MAXN*2];
int n,head[MAXN],cntEdge,col[MAXN],dp1[MAXN],dp2[MAXN];
inline int max(int x,int y){
	return x>y?x:y;
}
inline void add(int u,int v){
	cntEdge++;
	e[cntEdge].from=u,e[cntEdge].to=v;
	e[cntEdge].next=head[u],head[u]=cntEdge;
}
inline void dfs1(int u,int fa){
	dp1[u]=col[u];
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to; if(v==fa) continue;
		dfs1(v,u); dp1[u]+=max(dp1[v],0);
	}
}
inline void dfs2(int u,int fa){
	if(u!=1) dp2[u]=max(dp2[fa]-max(0,dp1[u]),0)+dp1[u];
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to; if(v==fa) continue;
		dfs2(v,u);
	}
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++){
		int a=read();
		col[i]=(a==0)?-1:1;
	}
	for(int i=1;i<=n-1;i++){
		int u=read(),v=read();
		add(u,v); add(v,u);
	}
	dfs1(1,0); dp2[1]=dp1[1];
	dfs2(1,0);
	for(int i=1;i<=n;i++) write(dp2[i]),putchar(' ');
	puts("");
}

4. [SCOI2005]互不侵犯

题目大意

给定 \(n\times n\) 的一张棋盘,在上面放置了 \(k\) 个国王使得国王所在格两两没有公共点(即以国王为中心的九宫格内无其他国王),求合法的方案总数。

\(n\leq 9\)\(k\leq n^2\)

分析

显然的状压 dp。

状压 dp 的套路是通过位运算快速地枚举每一种合法状态,其实是很暴力的一种 dp。

进行状压的原理就是我把每一行的状态种类给存下来了事。这里每一行的合法状态很少,可以考虑用每一行的合法状态作为转移参数之一。

考虑,如果我想做第 \(i\) 行,并且前 \(i\) 行已经填入了 \(k\) 个国王的合法方案数。其与 \(i-1\) 行有关。

定义 \(F(i,S,t)\) 表示第 \(i\) 行的状态为 \(S\),前 \(i\) 行一共填入 \(t\) 个国王的合法方案数,则:

\[F(i,S,t)\underset{\operatorname{check}(S,T)=\operatorname{true}}{=}\sum F(i-1,T,t-x) \]

其中 \(x\) 表示第 \(i\) 行这个状态 \(S\) 的填入的国王个数。那么暴力的转移即可。

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=15;
int n,k,ans,dp[MAXN][MAXN*MAXN][MAXN*MAXN],s[MAXN*MAXN],num[MAXN*MAXN];
inline bool check(int x,int y){
	if(s[x]&s[y]) return false;
	if(s[x]&(s[y]<<1)) return false;
	if(s[x]&(s[y]>>1)) return false;
	return true;
}
signed main()
{
	n=read(),k=read();
	int tot=0;
	for(int i=0;i<(1<<n);i++){
		if(i&(i<<1)) continue;
		int cnt=0;
		for(int j=0;j<n;j++) if(i&(1<<j)) cnt++;
		s[++tot]=i; num[tot]=cnt;
	}
	dp[0][1][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=tot;j++)
			for(int a=0;a<=k;a++){
				if(a>=num[j])
					for(int t=1;t<=tot;t++)
						if(check(t,j)) dp[i][j][a]+=dp[i-1][t][a-num[j]];
			}
	for(int i=1;i<=tot;i++) ans+=dp[n][i][k];
	write(ans),puts("");
}

5. [USACO06NOV]Corn Fields G

题目大意

给定一个 \(n\times m\) 的网格地,其中部分地能够选,标记为 1,其余地不能够选,标记为 0。求选定地中没有相邻的地的选择方案数,对 \(10^8\) 取模。

\(1\leq n,m\leq 12\)

分析

跟上面一题差不多。方程是

\[F(i,S)\underset{\operatorname{check}(S,T)=\operatorname{true}}{=}\sum F(i-1,T) \]

只不过你的 check 多判一下跟地图匹配的就行了。

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
//#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
//#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=15,MOD=1e8;
int n,m,a[MAXN][MAXN],dp[MAXN][(1<<MAXN)],ans,s[1<<MAXN],cntState;
inline bool check(int line,int st,int lst){
	if(st&lst) return false;
	for(int i=0;i<m;i++){
		if((a[line][i+1]==0)&&(st&(1<<i))) return false;
	}
	return true;
}
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			a[i][j]=read();
		}
	for(int i=0;i<(1<<m);i++){
		if(i&(i<<1)) continue;
		else s[++cntState]=i;
	}
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=cntState;j++){
			for(int k=1;k<=cntState;k++){
				if(check(i,s[j],s[k]))
					dp[i][s[j]]+=dp[i-1][s[k]],dp[i][s[j]]%=MOD;
			}
		}
	}
	for(int i=1;i<=cntState;i++) ans+=dp[n][s[i]],ans%=MOD;
	write(ans),puts("");
	return 0;
}

6. [NOI2001] 炮兵阵地

题目大意

给定地图,一个炮兵打的范围是上二下二左二右二,炮兵只能放在特定格子内,求给定图中无冲突的放置方案中放炮兵的最多数量。

\(n\leq 100\)\(m\leq 10\)

分析

状压 dp 的题目难点主要在于判断合法状态。然后这题就是多预处理一个每个状态的选中数量,做一遍 dp 就行了。

但卡空间就是说,发现每一行只与前两行有关,直接滚动数组就行力。

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
//#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
//#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=10;
char a[MAXN*MAXN][MAXN];
int n,m,ans,cntState,dp[5][1<<MAXN][1<<MAXN],s[1<<MAXN],num[1<<MAXN];
inline bool check(int line,int cur,int bf1,int bf2){
	for(int i=0;i<m;i++)
		if((a[line][i+1]=='H')&&(cur&(1<<i))) return false;
	if(cur&bf1) return false; if(cur&bf2) return false;
	return true;
}
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>a[i][j];
	for(int i=0;i<(1<<m);i++){
		if((i&(i<<1))||(i&(i<<2))) continue;
		s[++cntState]=i;
		for(int j=0;j<=m;j++) if(i&(1<<j)) num[cntState]++;
	}
	for(int i=1;i<=cntState;i++){
		bool flag=true;
		for(int j=0;j<m;j++)
			if((a[1][j+1]=='H')&&(s[i]&(1<<j))) flag=false;
		if(flag) dp[1][s[i]][0]=num[i];
	}
	for(int i=2;i<=n;i++){
		for(int j=1;j<=cntState;j++){
			for(int k=1;k<=cntState;k++){
				for(int l=1;l<=cntState;l++){
					if(check(i,s[j],s[k],s[l]))
						dp[i%3][s[j]][s[k]]=max(dp[(i-1)%3][s[k]][s[l]]+num[j],dp[i%3][s[j]][s[k]]);
				}
			}
		}
	}
	for(int i=1;i<=cntState;i++)
		for(int j=1;j<=cntState;j++)
			ans=max(ans,dp[n%3][s[i]][s[j]]);
	write(ans),puts("");
}

7. [Code+#1]找爸爸

题目大意

给定两个由 ATGC 组成的字符串 \(s_1\)\(s_2\)。由于两个字符串长度可能不同,你可能需要在其中添加空格使得最终两个字符串长度相等。你需要最大化这两个字符串的匹配值。

对长度相等的这两个字符串进行逐位匹配,如果某一位上两个字符都不是空格,匹配值需要加上这两个字符所对的匹配值(可能有负数)。如果有长度为 \(k\) 的连续空格,定义这一段的总匹配值为 \(g(k)=-a-(k-1)\times b\)

\(|s_1|+|s_2|\leq 3\times 10^3\)

分析

看上去是个字符串题,实际上是个 dp。(这里原因比较显然,因为匹配的权值是给定的,不能通俗的直接进行运算,不过这个还是比较难想到的。)

我们定义 \(dp_{i,j}\)\(s_1\) 匹配到第 \(i\) 位,\(s_2\) 匹配到第 \(j\) 位所得的结果。

试着写一写:\(dp_{i,j}=dp_{i-1,j-1}+d_{s_{1_i},s_{2_j}}\),我们发现无法处理(无论是前继位还是这一位的)空格情况。那么再加一维来考虑空格。

显然,如果在匹配的某一位上 \(s_1\)\(s_2\) 同时出现空格,这显然是不优的,原因在于多浪费了 \(-a\) 或者是 \(-b\)。所以无论匹配到哪一位,空格的可能性只局限于三种:

  • \(s_1\) 有空格。
  • \(s_2\) 有空格。
  • \(s_1\)\(s_2\) 都没有空格。

我们另加一维 \(dp_{i,j,k}\),让 \(k\in\{0,1,2\}\),记录这种状态下的空格情况。不妨让 \(k=0\) 表示都没有空格,\(k=1\) 表示 \(s_1\) 有空格,\(k=2\) 表示 \(s_2\) 有空格。接着做 dp。

我们发现那个定义连续空格的 \(g\) 实际上跟 dp 非常适配。我们只需要找到起始点,\(-a\),接下来全是 \(-b\)。而起始点在 dp 中只要与前面的 \(k\) 不同即可。而连续的情况就是 \(k\) 相等。那么 dp 方程就很好写了。

\[\begin{cases}dp_{i,j,0}=\max\{dp_{i-1,j-1,0},dp_{i-1,j-1,1},dp_{i-1,j-1,2}\}+d_{s_{1_i},s_{2_j}}\\dp_{i,j,1}=\max\{dp_{i,j-1,1}-b,dp_{i,j-1,2}-a,dp_{i,j-1,0}-a\}\\dp_{i,j,2}=\max\{dp_{i-1,j,1}-a,dp_{i-1,j,2}-b,dp_{i-1,j,0}-a\}\end{cases} \]

这题还有一个坑点就是边界。首先 \(\forall x>0\) 不可能存在 \(dp_{x,0,1}\)\(dp_{0,x,2}\) 的情况。对于 \(dp_{0,x,1}\)\(dp_{x,0,2}\)\(g(x)\)

特别地,\(dp_{0,0,1}\)\(dp_{0,0,2}\) 也都不存在。按序模拟即可。

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) x=-x,putchar('-');
	if(x>=10) write(x/10);
	putchar(x%10+'0');
}
const int MAXN=3e3+5;
char str1[MAXN],str2[MAXN];
int n,m,A,B,d[5][5],dp[MAXN][MAXN][3];
map<char,int> t;
inline int tmax(int a,int b,int c){
	return max(a,max(b,c));
}
signed main()
{
	scanf("%s",str1+1),scanf("%s",str2+1);
	n=strlen(str1+1),m=strlen(str2+1);
	for(int i=1;i<=4;i++)
		for(int j=1;j<=4;j++) d[i][j]=read();
	A=read(),B=read();
	t['A']=1,t['T']=2,t['G']=3,t['C']=4;
	for(int i=1;i<=n;i++) dp[i][0][2]=-A-(i-1)*B,dp[i][0][1]=dp[i][0][0]=-0x7f7f7f7f;
	for(int i=1;i<=m;i++) dp[0][i][1]=-A-(i-1)*B,dp[0][i][2]=dp[0][i][0]=-0x7f7f7f7f;
	dp[0][0][1]=-0x7f7f7f7f,dp[0][0][2]=-0x7f7f7f7f;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			int x=t[str1[i]],y=t[str2[j]];
			dp[i][j][0]=tmax(dp[i-1][j-1][0],dp[i-1][j-1][1],dp[i-1][j-1][2])+d[x][y];
			dp[i][j][1]=tmax(dp[i][j-1][0]-A,dp[i][j-1][1]-B,dp[i][j-1][2]-A);
			dp[i][j][2]=tmax(dp[i-1][j][0]-A,dp[i-1][j][1]-A,dp[i-1][j][2]-B);
		}
	write(tmax(dp[n][m][0],dp[n][m][1],dp[n][m][2])),puts("");
	return 0;
}

8. 樱花

题目大意

起始时间 \(T_s\),终止时间 \(T_e\),需要在终止时间前欣赏 \(n\) 棵樱花树中一部分,其中欣赏樱花树需要时间 \(t_i\),欣赏这棵樱花树带来的收益 \(c_i\),一棵樱花树最多可以看 \(a_i\)\(\infin\) 次,其中 \(\infin\)\(0\) 表示。问最大收益之和。

\(n\leq 10^4\)

分析

混合背包板子。

所拥有的时间即为背包容量,每次欣赏樱花时间即为物品体积,收益即为物品价值。有 01 背包,多重背包和完全背包。

由于 01 背包属于特殊的多重背包,这里一并分析。在做多重背包的时候需要倒序;将物品数量拆分为多个,然后按照 01 背包做即可。

完全背包正序做即可。这里俩背包方程都是 \(f_j=\max\{f_j,f_{j-w_i}+v_i\}\)

代码

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
//#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
//#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=1e6+5;
int tsth,tstmin,tenh,tenmin,n,w,dp[MAXN],ans;
struct obj{
    int type,num,val,cst;
}a[MAXN];
signed main()
{
    tsth=read(),tstmin=read(),tenh=read(),tenmin=read();
    w=(tenh-tsth)*60+tenmin-tstmin;
//    write(w),puts("");
    n=read();memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;i++){
        a[i].cst=read(),a[i].val=read(),a[i].num=read();
        if(a[i].num==0) a[i].type=1;else a[i].type=0;
    }
    for(int i=1;i<=n;i++){
    	if(a[i].type==1){
    		for(int j=a[i].cst;j<=w;j++)
    			dp[j]=max(dp[j],dp[j-a[i].cst]+a[i].val);
    	}
    	else{
    		for(int j=1;j<=a[i].num;j++)
    			for(int k=w;k>=a[i].cst;k--)
    				dp[k]=max(dp[k],dp[k-a[i].cst]+a[i].val);
    	}
    }
    for(int i=0;i<=w;i++) ans=max(ans,dp[i]);
    write(ans),puts("");
}

T 了两个点,需要二进制优化或者单调队列优化;开了 O2 就过了。

9. 关路灯

题目大意

有一个人,初始时位于位置 \(c\),在数轴上有 \(n\) 个点,位于 \(x_i\),如果没有被访问,每个点每个单位时间产生 \(W_i\) 的代价;如果被访问过则不产生代价。每个单位时间最多移动一个单位距离。以开始移动作为时刻原点,问访问所有点最小代价总和。

分析

观察到这个人只要经过一个路灯,必定会把这个路灯关掉以减小费用,因此所有被访问过的点都是连续的。这就使得我们所有访问过的点构成一条“线段”。我们不妨假设这条线段的左端点是第 \(l\) 个路灯的标号,右端点是第 \(r\) 个路灯的标号,对其进行区间 dp,计算 \(dp_{l,r}\) 为访问区间 \([l,r]\) 所需要的最小代价。

容易发现我们无法确定计算出的 \(dp_{l,r}\) 时,人在左端点还是右端点,而此时此刻的位置与转移下一状态有关,因此需要多开一维记录当前时刻所处位置。不妨设 \(dp_{l,r,1}\) 为当前时刻处于 \(l\) 点,\(dp_{l,r,2}\) 为当前时刻处于 \(r\) 点。

对于每一个点有两种决策,即继续向当前方向前进;或者调转方向。在方程中讨论这两种情况即可。

\[\begin{cases}dp_{l,r,1}=\min\{dp_{l+1,r,1}+\operatorname{cost}((l+1)\to l),dp_{l+1,r,2}+\operatorname{cost}(r\to l)\}\\ dp_{l,r,2}=\min\{dp_{l,r-1,2}+\operatorname{cost}((r-1)\to r),dp_{l,r-1,1}+\operatorname{cost}(l\to r)\}\end{cases} \]

非常凌乱。

\(\operatorname{cost}\) 函数的具体计算,用此时还没有被关闭的所有灯的总功率之和(可以用前缀和解决),乘上时间(即位置坐标之差)。

这是一道不需要枚举中转点的区间 dp,比较特殊(实质上中转点即与断电相邻的两个点,扩展的方式有点类似 TJOI 线段)。

代码

这里我犯的错误是误以为消耗功率的电灯是由位移的起点终点来计算的,实际上需要结合整段线段的两端点计算。

#include<bits/stdc++.h>
#define HohleFeuerwerke using namespace std
#pragma GCC optimize(3,"Ofast","-funroll-loops","-fdelete-null-pointer-checks")
#pragma GCC target("ssse3","sse3","sse2","sse","avx2","avx")
#define int long long
HohleFeuerwerke;
inline int read(){
	int s=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) s=s*10+c-'0';
	return s*f;
}
inline void write(int x){
	if(x<0) putchar('-'),x=-x;
	if(x>=10) write(x/10);
	putchar('0'+x%10);
}
const int MAXN=55,INF=0x7f7f7f;
int n,c,dp[MAXN][MAXN][3],sum[MAXN];
struct light{
	int pos,W;
}x[MAXN];
inline int cost(int from,int to,int cstl,int cstr,int type){
	//type=1 from i(left) to j(right)
	//type=2 otherwise reverse
	//note: i<j (always)
	if(type==1) return abs(x[from].pos-x[to].pos)*(sum[n]-sum[cstr-1]+sum[cstl-1]);
	if(type==2) return abs(x[from].pos-x[to].pos)*(sum[n]-sum[cstr]+sum[cstl]);
}
signed main()
{
	n=read(),c=read(),memset(dp,INF,sizeof(dp));
	for(int i=1;i<=n;i++) x[i].pos=read(),x[i].W=read();
	for(int i=1;i<=n;i++) sum[i]=sum[i-1]+x[i].W;
	dp[c][c][1]=0,dp[c][c][2]=0;
	for(int len=2;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			dp[l][r][1]=min(dp[l+1][r][1]+cost(l,l+1,l,r,2),dp[l+1][r][2]+cost(l,r,l,r,2));
			dp[l][r][2]=min(dp[l][r-1][2]+cost(r-1,r,l,r,1),dp[l][r-1][1]+cost(l,r,l,r,1));
//in function "cost" 
//			if(type==1) return (x[j].pos-x[i].pos)*(sum[n]-sum[j-1]+sum[i-1]);
//			if(type==2) return (x[j].pos-x[i].pos)*(sum[n]-sum[j]+sum[i]);
//    		printf("%d %d %d %d\n",l,r,dp[l][r][1],dp[l][r][2]);
		}
	}
//  for(int len=2;len<=n;len++)
//    	for(int l=1;l+len-1<=n;l++){
//    		int r=l+len-1;
//  		printf("%d %d %d %d\n",l,r,dp[l][r][1],dp[l][r][2]);
//	}
//	write(dp[1][n][1]),putchar(' '),write(dp[1][n][2]),puts("");
	write(min(dp[1][n][1],dp[1][n][2])),puts("");
}
posted @ 2021-05-05 21:51  _HofFen  阅读(76)  评论(0编辑  收藏  举报