DP 杂题

dp 的转移一般有两种:填表、刷表。

填表是用用过去的状态推出现在的状态,而刷表是用现在的状态推出以后的状态。

有时候如果只定义一个状态发现难以转移,可以考虑定义多个状态,存储不同信息来方便转移。

P2577 [ZJOI2004]午餐

$\texttt{solution}$

想到贪心:

吃饭慢的先打饭节约时间, 所以先将人按吃饭时间从大到小排序。

状态:

\(f[i][j]\) 表示前 \(i\) 个人,在 \(1号\) 窗口打饭总时间 \(j\) ,最早吃完饭的时间。

我们可以发现 \(j+k\) 等于前 \(i\) 个人打饭总和( \(sum[i]\) ),\(k = sum(i)-j\) ,所以可以省去一维。

转移:

  1. 将第 \(i\) 个人放在 \(1\) 号窗口:(前提:\(j \ge s[i].a\)

\[dp[i][j] = \min{(dp[i][j], \max{(dp[i-1][j-s[i].a], j+s[i].b)})} \]

  1. 将第 \(i\) 个人放在 \(2\) 号窗口:

\[dp[i][j] = \min{(dp[i][j], \max{(dp[i-1][j], sum[i]-j+s[i].b)})} \]

核心代码:

bool cmp(Data x,Data y){ return x.b>y.b; }

sort(p+1,p+n+1,cmp);
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+p[i].a;
memset(dp,inf,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)
{
	 for(int j=0;j<=sum[i];j++)
	 {
	 	 if(j>=p[i].a) dp[i][j]=min(dp[i][j],max(dp[i-1][j-p[i].a],j+p[i].b));
	 	 dp[i][j]=min(dp[i][j],max(dp[i-1][j],sum[i]-j+p[i].b));
	 }
}
for(int i=0;i<=sum[n];i++) ans=min(ans,dp[n][i]);

P1026 统计单词个数

$\texttt{solution}$

难点:

  1. 双重 \(\operatorname{DP}\)

  2. 字符串处理 \(+\) 难以理解的题意 。

状态:

  1. \(sum[l][r]\) 表示从 \(i\)\(j\) 的单词数 。可以直接枚举区间和单词(字符串)转移。

  2. \(dp[i][k]\) 表示第 \(i\) 个位置,分了 \(k\) 块,能得到的最多的单词数 。

转移:

\[dp[i][k]=\max_{p=k}^{p<i}{\{ dp[i][k] , dp[p][k-1]+sum[p+1][i] \}} \]

代码:

bool check(int l,int r)
{
	 string c=s.substr(l,r-l+1);
	 for(int i=1;i<=t;i++) if((c.find(a[i]))==0) return true;
	 return false;
}
void pre()
{
	 for(int r=1;r<=len;r++)
	 	 for(int l=r;l>=1;l--)
	 	 {
	 	 	 sum[l][r]=sum[l+1][r];
	 	 	 if(check(l,r)) sum[l][r]++;
		 }
}
void solve()
{
	 for(int i=1;i<=k;i++) dp[i][i]=dp[i-1][i-1]+sum[i][i];
	 for(int i=1;i<=len;i++) dp[i][1]=sum[1][i];
	 for(int i=1;i<=len;i++) for(int k=1;k<=min(k,i-1);k++) for(int p=k;p<i;p++)
	 	 dp[i][k]=max(dp[i][k],dp[p][k-1]+sum[p+1][i]);
	 printf("%d\n",dp[len][k]);
}
void init()
{
	 cin>>w>>k;
	 string c;
	 for(int i=1;i<=w;i++) cin>>c,s+=c;
	 len=s.length(),s=" "+s;
	 cin>>t;
	 for(int i=1;i<=t;i++) cin>>a[i];
}

P2679 子串

$\texttt{solution}$

状态:

\(dp[i][j][k][0/1]\) 表示 \(A\) 的前 \(i\) 个字符和字符串 \(B\) 的前 \(j\) 个字符用了 \(k\) 个子串,\(A\)\(i\) 为取或不取的合法方案数。

初始化:对于 \(1 \le i \le n\)\(dp[i][0][0][0] = 0\)

转移:

\[dp[i][j][k][0]=(dp[i-1][j][k][0]+dp[i-1][j][k][1])\mod{1e9+7} \]

\[dp[i][j][k][1]=(dp[i-1][j-1][k][1]+dp[i-1][j-1][k-1][0]+dp[i-1][j-1][k-1][1])\mod{1e9+7} \]

代码:

f[0][0][0]=1;
for(int i=1;i<=n;i++)
{
	 memset(g,0,sizeof(g)),g[0][0][0]=1;
	 for(int j=1;j<=min(i,m);j++)
	 	 for(int k=1;k<=min(j,p);k++)
	 	 {
	 	 	 g[j][k][0]=(f[j][k][0]+f[j][k][1])%mod;
	 	 	 if(a[i]==b[j]) g[j][k][1]=((f[j-1][k][1]+f[j-1][k-1][0])%mod+f[j-1][k-1][1])%mod;
		 }
	 memcpy(f,g,sizeof(f));
}
printf("%d\n",(f[m][p][0]+f[m][p][1])%mod);

P1052 过河

$\texttt{solution}$

难点:

离散化

\(len\) 的范围太大,无法作为数组下标,所以先离散化,再 \(\operatorname{DP}\) 。两点间的距离 \(d\) 大于 \(t\) 时,一定可以由 \(d\%t\) 跳过来,所以最多只需要 \(t+d\%t\) 种距离的状态就可以表示这两个石子之间的任意距离关系。

代码:

bool cmp(int x,int y){ return x<y; }

len=rd();
s=rd(),t=rd(),m=rd();
for(int i=1;i<=m;i++) a[i]=rd();
a[m+1]=len;
sort(a+1,a+m+1,cmp);
for(int i=1;i<=m+1;i++)
{
	 if(a[i]-a[i-1]>t) cnt+=(a[i]-a[i-1])%t+t;
	 else cnt+=a[i]-a[i-1];
	 val[cnt]=1;
}
memset(dp,inf,sizeof(dp)),dp[0]=0;
for(int i=1;i<=cnt+t-1;i++) for(int j=s;j<=t;j++)
 	 if(i>=j) dp[i]=min(dp[i],dp[i-j]+val[i]);
for(int i=cnt;i<=cnt+t-1;i++) ans=min(ans,dp[i]);
printf("%d\n",ans);

P5664 Emiya 家今天的饭

$\texttt{solution}$

首先考虑列的限制,必然 有且只有一列不合法 的:因为不可能有不同的两列数量都 超过总数的一半\(ans=\) 总状态 \(-\) 不合法状态

计算不合法方案:

计算列的不合法方案数:每行选不超过一个的方案数 \(-\) 每行选不超过一个,且某一列选了超过一半的方案数。可以发现每一列都是独立的,可以枚举当某一列( 记为 \(cal\) )不合法时的方案再相加。

状态:

先设 \(dp[i][j][k]\) 表示表示对于 \(col\) 这一列,前 \(i\) 行在 \(col\) 列中选了 \(j\) 个,在其他列中选了 \(k\) 个的非法方案数。令 \(sum[i]\) 为第 \(i\) 行的总和 。

转移:

\[dp[i][j][k]=dp[i-1][j][k] + a[i][cal]\times dp[i-1][j-1][k]+(sum[i]-a[i][cal])\times dp[i-1][j][k-1] \]

复杂度:\(O(mn^3)\) ,可以得到 \(84pts\)

考虑优化:

在不合法情况的转移过程中,我们并不关心 \(j\)\(k\) 的具体数值,而只关心相对的大小关系。

状态:

\(dp[i][j]\) 表示前 \(i\) 行,当前列的数比其他列的数多了 \(j\) 个 。

转移:

\[dp[i][j]=dp[i-1][j]+a[i][cal]\times dp[i-1][j-1]+(sum[i]-a[i][cal])\times dp[i-1][j+1] \]

复杂度:\(O(mn^2)\) ,复杂度在时间范围内。

统计总方案数:

状态:

\(cnt[i][j]\) 为前 \(i\) 行共选了 \(j\) 个数的方案数 。

转移:

\[cnt[i][j]=cnt[i-1][j]+sum[i]\times cnt[i-1][j-1] \]

总方案就是 \(\sum_{i=1}^n {g[n][i]}\)

复杂度:\(O(n^2)\) ,可以通过这道题。

代码:

for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
 	 a[i][j]=rd(),sum[i]=(sum[i]+a[i][j])%mod;
for(int cal=1;cal<=m;cal++)
{
	 memset(dp,0,sizeof(dp)),dp[0][n]=1;
	 for(int i=1;i<=n;i++) for(int j=n-i;j<=n+i;j++)
	 	 dp[i][j]=(dp[i-1][j]+dp[i-1][j-1]*a[i][cal]%mod+dp[i-1][j+1]*((sum[i]+mod-a[i][cal])%mod)%mod)%mod;
	 for(int j=1;j<=n;j++) unok=(unok+dp[n][j+n])%mod;
}
cnt[0][0]=cnt[1][0]=1;
for(int i=1;i<=n;i++,cnt[i][0]=cnt[i-1][0]) for(int j=1;j<=n;j++)
 	 cnt[i][j]=(cnt[i-1][j]+sum[i]*cnt[i-1][j-1]%mod)%mod;
for(int i=1;i<=n;i++) ans=(ans+cnt[n][i])%mod;
printf("%lld\n",(ans+mod-unok)%mod);

P2258 子矩阵

$\texttt{solution}$

题意:

\(n\times m\) 的矩阵中选取 \(r\times c\) 的子矩阵(可以跳行 \(/\) 跳列间隔选取),使子矩阵相邻两元素的差之和最小 。

题解:

算法:枚举 \(+ \operatorname{DP}\)

考虑到 \(n,m\) 比较小,所以先枚举选出那些行 ,这里的复杂度为 \(O(C_n^r)\)

之后考虑 \(\operatorname{DP}\) 选取列 :

1. 预处理:

处理出单独选一列,这 \(r\) 行上下之间的差之和( \(lie[i]\) ) 。

再处理如果选了第 \(i\) 列和第 \(j\) 列,这两列横着的 \(r\) 行元素的差值之和( \(hang[i][j]\) ) 。

2. \(\operatorname{DP}\)

\(dp[i][j]\) 表示在前 \(i\) 行中选了 \(j\) 行的最小差值之和 。

转移:

  • \(j=1\) 时,\(dp[i][j]\) 为仅选第 \(i\) 列的差值之和 。

  • 其他情况,可以枚举 \(k=[j-1,i-1]\) ,考虑子矩阵的第 \(j-1\) 选择为 \(k\) 的情况下的代价是多少,并取 \(\min\) 转移。

代码:

int ans=inf;
void solve()
{
	 for(int i=1;i<=m;i++) lie[i]=0;
	 for(int i=1;i<m;i++) for(int j=i+1;j<=m;j++) hang[i][j]=0;
	 for(int i=1;i<=m;i++) for(int j=2;j<=r;j++) lie[i]+=abs(a[ch[j]][i]-a[ch[j-1]][i]);
	 for(int i=1;i<m;i++) for(int j=i+1;j<=m;j++) for(int k=1;k<=r;k++)
	 	 hang[i][j]+=abs(a[ch[k]][j]-a[ch[k]][i]);
	 for(int i=1;i<=m;i++) for(int j=1,Limit=min(i,c);j<=Limit;j++)
	 {
	 	 if(j==1) dp[i][j]=lie[i];
	 	 else
	 	 {
	 	 	 dp[i][j]=inf;
	 	 	 for(int k=j-1;k<i;k++) dp[i][j]=min(dp[i][j],dp[k][j-1]+lie[i]+hang[k][i]);
		 }
	 }
	 for(int i=c;i<=m;i++) ans=min(ans,dp[i][c]);
}
void dfs(int Left,int st)
{
	 if(!Left)
	 {
	 	 solve();
	 	 return;
	 }
	 if(st>n) return;
	 for(int i=st;i<=n-Left+1;i++) ch[r-Left+1]=i,dfs(Left-1,i+1);
}

dfs(r,1);
printf("%d\n",ans);

P6064 [USACO05JAN]Naptime G

$\texttt{solution}$

算法:线性 \(\operatorname{DP}\)

列出基础转移方程

先不考虑第 \(n\) 个小时与次日第 \(1\) 个小时连续 。

\(dp[i][j][0/1]\) 表示在第 \(i\) 个小时,已经在床上躺了 \(j\) 个小时,\(0\) 表示这个小时没在床上,\(1\) 表示这个小时正躺在床上 。

转移方程:( 初始值 \(dp[1][0][0]=dp[1][1][1]=0\) ,其他为 \(-\inf\)

\[\begin{cases}dp[i][j][0]=\max{\{dp[i-1][j][0],dp[i-1][j][1]\}}\\dp[i][j][1]=\max{\{dp[i-1][j-1][0],dp[i-1][j-1][1]+u[i]\}}\end{cases} \]

目标:\(\min{\{dp[n][b][0],dp[n][b][1]\}}\)

完善转移方程

考虑第 \(n\) 个小时与次日第 \(1\) 个小时连续 ,即强制第第 \(n\) 个小时睡觉 。

初始值:\(dp[1][0][0]=0,dp[1][1][1]=u[1]\) ,其他为 \(-\inf\)

目标:\(dp[n][m][1]\)

最终答案为两种情况的较大值。

代码:

#define inf 0x7f7f7f7f
#define Maxn 3835
int n,b,ans,a[Maxn],dp[Maxn][Maxn][2];

// 一下代码片段插入在 main 函数中 
n=rd(),b=rd();
for(int i=1;i<=n;i++) a[i]=rd();
memset(dp,-inf,sizeof(dp)),dp[1][1][1]=dp[1][0][0]=0;
for(int i=2;i<=n;i++)
{
 	 dp[i][0][0]=dp[i-1][0][0];
	 for(int j=1;j<=b;j++)
	 {
	 	 dp[i][j][0]=max(dp[i-1][j][1],dp[i-1][j][0]);
	 	 dp[i][j][1]=max(dp[i-1][j-1][0],dp[i-1][j-1][1]+a[i]);
	 }
}
ans=max(dp[n][b][0],dp[n][b][1]);
memset(dp,-inf,sizeof(dp)),dp[1][1][1]=a[1],dp[1][0][0]=0;
for(int i=2;i<=n;i++)
{
 	 dp[i][0][0]=dp[i-1][0][0];
 	 for(int j=1;j<=b;j++)
 	 {
 	 	 dp[i][j][0]=max(dp[i-1][j][1],dp[i-1][j][0]);
 	 	 dp[i][j][1]=max(dp[i-1][j-1][0],dp[i-1][j-1][1]+a[i]);
	 }
}
ans=max(ans,dp[n][b][1]);
printf("%d\n",ans);

P1043 数字游戏

$\texttt{solution}$

算法:环形 \(\operatorname{DP}\)

方法:

  1. 破环成连,把环变为两倍,统计答案的时候把答案扫一遍。

  2. \(dp[l][r][p]\) 表示把 \(l\)\(r\) 这一个区间分为 \(p\) 段的最小 \(/\) 最大代价 。

代码:

int ansmin=inf,ansmax;
int MAX[Maxn][Maxn][Maxm],MIN[Maxn][Maxn][Maxm];
inline int mod(int x) { return ((x%10)+10)%10; } // 保证是正数

n=rd(),m=rd();
for(int i=1;i<=n;i++) a[i]=a[i+n]=rd();
for(int i=1;i<=n*2;i++) sum[i]=sum[i-1]+a[i];
memset(MIN,inf,sizeof(MIN)); // ↓ 初始化一些状态
for(int l=1;l<=n*2;l++) for(int r=l;r<=n*2;r++) MAX[l][r][1]=MIN[l][r][1]=mod(sum[r]-sum[l-1]);
for(int p=2;p<=m;p++) for(int l=1;l+p-1<=n*2;l++) for(int r=l+p-1;r<=n*2;r++) for(int k=l+p-2;k<r;k++)
{
 	 MAX[l][r][p]=max(MAX[l][r][p],MAX[l][k][p-1]*mod(sum[r]-sum[k]));
 	 MIN[l][r][p]=min(MIN[l][r][p],MIN[l][k][p-1]*mod(sum[r]-sum[k]));
} // k 是枚举转移点
for(int i=1;i<n;i++) ansmin=min(ansmin,MIN[i][i+n-1][m]),ansmax=max(ansmax,MAX[i][i+n-1][m]); // 扫一遍答案
printf("%d\n%d\n",ansmin,ansmax);

P2331 [SCOI2005]最大子矩阵

$\texttt{solution}$

题意:

这里有一个 \(n\times m\) 的矩阵,请你选出其中 \(k\) 个子矩阵,使得这个 \(k\) 个子矩阵分值之和最大。

注意:选出的 \(k\) 个子矩阵不能相互重叠。

其中,\(1\le n\le 100,1\le m\le 2,1\le k\le 10\)

题解:

注意到 \(m\) 比较小,分为几类:

\(m=1\) 时,是普通的最大连续字段和,只不过是 \(k\) 个:

\(dp[i][j]\) 表示前 \(i\) 个数中取出 \(j\) 个矩形的最大和

转移:

  • 选:

\[dp[i][j]=\max_{l=1}^{l<i}{\{dp[l][j-1]+s[i]-s[l-1]\}} \]

  • 不选:

\[dp[i][j]=\max(dp[i][j],dp[i-1][j]) \]

复杂度 \(O(n^2\times k)\)

\(m=2\) 时,设 \(f[i][j][k]\) 表示第一列选到第 \(i\) 个数,第二列选到第 \(j\) 个数时,总共 \(k\) 个子矩形的答案

转移有 \(4\) 种情况

  • 当这一位什么都不做的时候:

\[f[i][j][k]=\max(f[i-1][j][k],f[i][j-1][k]) \]

  • 当仅选取第一列的某段区间时:

\[f[i][j][k]=\max_{l=1}^{l<i}{\{f[l][j][k-1]+sum[i][1]-sum[l-1][1]\}} \]

  • 当仅选取第二列的某段区间时:

\[f[i][j][k]=\max_{l=1}^{l<j}{\{f[i][l][k-1]+sum[j][2]-sum[l-1][2]\}} \]

  • \(i==j\) 时,可以选取两列一起的

\[f[i][j][k]=\max_{l=1}^{l< \min(i,j)}{\{f[l][l][k]+sum[i][1]+sum[i][2]-sum[l-1][1]-sum[l-1][2]\}} \]

最后所有情况取 \(\max\)

复杂度 \(O(n^3\times k)\)

CF1174E Ehab and the Expected GCD Problem

$\texttt{solution}$

首先考虑在权值最大时第一个数一定为 \(2^x2^y\) ,且 \(y\le 1\)

再分析往下填的数,考虑 \(dp[i][j][k]\) 表示填到第 \(i\) 个,前缀 \(\gcd\)\(2^j3^k\) 时的方案数。

可以填 \(2^j3^k,2^{j-1}3^k,2^j3^{k-1}\) 的倍数,分别讨论。

CF149D Coloring Brackets

$\texttt{solution}$

(需要想到区间 \(\text{DP}\)

\(dp(l,r,i,j)\) 表示区间 \([l,r]\) 中,左端点颜色为 \(i\) ,右端点颜色为 \(j\) 的涂色方案数。

分为三类情况:

  • \(l+1=r\) :直接赋值。
  • \(match(l)=r\) :由 \(dp(l+1,r-1,,)\) 转移而来。
  • \(match(l)!=r\) :由 \(dp(l,match(l),,)\times dp(match(l)+1,r,,)\) 转移而来。

由于这一题的局部最优解与全局最优解之间没有直接方便的转移方式,所以使用递归的方式求出 \(\text{DP}\) 值。

P3592 [POI2015]MYJ

$\texttt{solution}$

因为 \(n\le 50,m\le 300\),所以考虑一个 \(O(n^3)\) 的算法,这样容易想到区间 \(\text{dp}\)

把付的钱离散化

\(cnt(i,j)\)\([l,r]\) 中,在 \(i\) 位置填颜色 \(j\) 的消费人数

\(dp(l,r,k)\) :在 \([l,r]\) 中最少的钱为 \(k\) 时的最大获得钱数

在转移时与 \(dp(l,r,k+1)\)\(\max\) ,因为 \(k\)\(k+1\)

记下这个状态最优时,【最少的钱的位置】与【最少的钱的钱的多少】

CF840C On the Bench

$\texttt{solution}$

\(p\) 除去所有平方因子,转化为相邻的 \(p\) 互不相同。

想象把数一个一个塞到原序列中。

\(dp(i,j,k)\) 放了 \(i\) 个,\(j\) 个相同且相邻,\(k\) 个与第 \(i\) 个数相同且相邻

为什么想到要这么假设呢?

为了让数字相同的一起处理,把 \(p\) 排好序,并且在颜色变化时记得更新~

起始状态 \(dp(0,0,0)\)

目标 \(dp(n,0,0)\)

处理出一个数和之前多少个数相同 ( \(pre\) )

若塞入后和左/右其一相同:

\[dp[i-1][j][k]\times(pre+pre-k[可以塞的位置])->dp[i][j+1][k+1] \]

若塞入后与左右都不同,但左右相同:

\[dp[i-1][j][k]\times(j-k[可以塞的位置])->dp[i][j-1][k] \]

若塞入后与左右都不同,且左右不同:

\[p[i-1][j][k]\times(i[总位置数]-(pre+pre-k)[情况 1 ]-(j-pre)[情况 2 ])->dp[i][j][k] \]

CF830D Singer House

$\texttt{solution}$

考虑把深度一个一个累加,去考虑怎样从上一个阶段转移到这一个阶段。

假设增加了一层,不妨假设用一个新的根节点合并两颗深度为 \(n-1\) 的子树(明显这样更好维护呀)

设此时的答案为 \(f_n\)

一个思路是考虑这条路径是否经过根节点,那么有几种情况:

这条路径只包含根节点;这条路径从下面某棵子树内一条路径连上来,再连接下去

乍一看似乎能做,但是我们会发现,从一棵子树中连上来的路径可能会连回同一棵子树,那么如果我们要算\(f_n\) ,就必须算出从深度为 \(n-1\) 的子树内选择两条不相交的路径的方案数 \(g_{n-1}\)

你可能会想继续讨论 \(g_n\) 的方案数,但是你会发现,你要算 \(g_n\) ,还得算深度为 \(n-1\) 的树种选三条不相交路径的方案数……

既然如此,我们观察一下数据范围, 不妨多设一维状态:

\(f_{n,k}\) 代表在深度为 \(n\) 的树中选择 \(k\) 条不相交路径的方案数。

看上去似乎变难了,毕竟原题只让我们求一条路径的方案数。

但是我们发现,这个“加强”版本似乎更好做了,因为转移变得十分简单:

\[f_{n,k}=\sum_{i+j=k-1}f_{n-1,i}\times f_{n-1,j} \]

\[+\sum_{i+j=k}f_{n-1,i}\times f_{n-1,j} \]

\[+\sum_{i+j=k}f_{n-1,i}\times f_{n-1,j}\times (2k) \]

\[+\sum_{i+j=k+1}f_{n-1,i}\times f_{n-1,j}\times (k+1)\times k \]

这四种情况分别是:根节点单独形成一条链、根节点不属于任何一条链、根节点与左右子树内某条链连在一起(分从链的尾端连上来和连到链的开头两种情况)、还有根节点从某条链上连上再连到另一条链上去。

参考

P5336 [THUSC2016]成绩单

$\texttt{solution}$

区间 dp,用辅助数组的方式方便转移。

发现 \(n\le 50\) 和区间问题,想到区间 dp。

加入我们设 \(f(l,r)\) 表示取完区间 \([l,r]\) 内的成绩单的最小代价,会发现难以转移,考虑一个辅助数组。

那么需要将一段区间分为多次,再合并后取走,这时设置多个维度来记录当前状态。

有一个比较显然的结论:成绩相差越小的越可能放在一起,不会出现成绩跳跃地选取。

\(dp(l,r,x,y)\) 表示在 \([l,r]\) 中,取走了若干区间后只剩下在 \([x,y]\) 之间成绩的代价(到达这个状态的代价)。

转移有两种:

  • \(r\) 以及最大 \(/\) 最小值删去。

  • 对于端点 \(k\in [l,r)\),删去 \([l,k]\)

我们可以通过这个辅助数组转移到答案。

$\texttt{code}$
#define Maxn 55
#define Maxval 1005
typedef long long ll;
int n,a,b,cnt;
int dp[Maxn][Maxn][Maxn][Maxn],g[Maxn][Maxn];
int t[Maxn],in_tr[Maxn],tr_in[Maxval];
bool cmp(int x,int y){ return x<y; }
int main()
{
	 n=rd(),a=rd(),b=rd();
	 for(int i=1;i<=n;i++) t[i]=in_tr[i]=rd();
	 sort(in_tr+1,in_tr+n+1,cmp);
	 cnt=unique(in_tr+1,in_tr+n+1)-in_tr-1;
	 for(int i=1;i<=cnt;i++) tr_in[in_tr[i]]=i;
	 for(int i=1;i<=n;i++) t[i]=tr_in[t[i]];
	 memset(dp,inf,sizeof(dp)),memset(g,inf,sizeof(g));
	 for(int i=1;i<=n;i++) dp[i][i][t[i]][t[i]]=0,g[i][i]=a;
	 for(int len=2;len<=n;len++) for(int l=1,r;l<=n-len+1;l++)
 	 {
 	 	 r=l+len-1;
 	 	 for(int i=1;i<=cnt;i++) for(int j=i;j<=cnt;j++)
 	 	 {
 	 	 	 dp[l][r][min(i,t[r])][max(j,t[r])]=
				 min(dp[l][r][min(i,t[r])][max(j,t[r])],dp[l][r-1][i][j]);
			 for(int k=l;k<r;k++)
			 	 dp[l][r][i][j]=min(dp[l][r][i][j],dp[l][k][i][j]+g[k+1][r]);
		 }
		 for(int i=1;i<=cnt;i++) for(int j=i;j<=cnt;j++)
		 	 g[l][r]=min(g[l][r],dp[l][r][i][j]+a+b*(in_tr[j]-in_tr[i])*(in_tr[j]-in_tr[i]));
	 }
	 printf("%d\n",g[1][n]);
	 return 0;
}

相似例题:

CF1107E Vasya and Binary String

$\texttt{solution}$

由于这题字符集比较小,所以可以用上一题的方式,设 \(dp(l,r,0/1,k)\) 表示将这个区间取到只剩下 \(k\)\(0\)\(k\)\(1\) 的最大收益。

之后可以 \(O(n^4)\) 转移。

$\texttt{code}$
#define Maxn 105
typedef long long ll;
int n;
char s[Maxn];
ll c[Maxn],f[Maxn];
ll g[Maxn][Maxn];
// answer
ll dp[Maxn][Maxn][2][Maxn];
int main()
{
	 n=rd(),scanf("%s",s+1);
	 for(int i=1,x;i<=n;i++)
	 {
	 	 x=rd(),c[i]=s[i]-'0';
	 	 for(int j=i;j<=n;j++) f[j]=max(f[j],f[j-i]+x);
	 }
	 memset(dp,-infll,sizeof(dp)),memset(g,-infll,sizeof(g));
	 for(int i=1;i<=n;i++)
	 {
	 	 g[i][i]=f[1];
		 dp[i][i][c[i]][1]=0,dp[i][i][c[i]][0]=f[1];
		 dp[i][i][c[i]^1][0]=0;
	 }
	 for(int len=2;len<=n;len++)
	 {
	 	 for(int l=1,r;l<=n-len+1;l++)
	 	 {
	 	 	 r=l+len-1;
 	 	 	 for(int p=1;p<=len;p++)
 	 	 	 {
 	 	 	 	 dp[l][r][c[r]][p]=maxll(dp[l][r][c[r]][p],dp[l][r-1][c[r]][p-1]);
 	 	 	 	 for(int k=l;k<r;k++) for(int opt=0;opt<2;opt++)
 	 	 	 	 	 dp[l][r][opt][p]=maxll(dp[l][r][opt][p],dp[l][k][opt][p]+g[k+1][r]);
			 }
			 for(int p=1;p<=len;p++) for(int opt=0;opt<=1;opt++)
			 	 g[l][r]=maxll(g[l][r],dp[l][r][opt][p]+f[p]);
		 }
	 }
	 printf("%lld\n",g[1][n]);
	 return 0;
}

然而这样的做法并不能解决下一道题,我们考虑这样一个 \(dp\):设 \(dp(l,r,k)\) 表示取到这个区间右端还有 \(k\) 个字符和 \(c_r\) 一致,将他们全部取玩的最大收益。

发现这样就可以摆脱字符集的限制,转移方程如下:(\(f(x)\) 表示删去长度为 \(x\) 的区间的最大收益)

  • \(r\) 以及它右边的相同字符删去,\(dp(l,r,k)=dp(l,r-1,0)+f(k+1)\)

  • 删去一段区间并接上上一个相同的字符,\(dp(l,r,k)=dp(l,p,k+1)+dp(p+1,r-1,0)\)

那么最终答案就是 \(dp(1,n,0)\)

$\texttt{code}$
#define Maxn 105
typedef long long ll;
int n;
char s[Maxn];
ll c[Maxn],f[Maxn];
ll dp[Maxn][Maxn][Maxn];
int main()
{
	 n=rd(),scanf("%s",s+1);
	 for(int i=1,x;i<=n;i++)
	 {
	 	 x=rd(),c[i]=s[i]-'0';
	 	 for(int j=i;j<=n;j++) f[j]=max(f[j],f[j-i]+x);
	 }
	 for(int len=1;len<=n;len++) for(int l=1,r;l<=n-len+1;l++)
 	 {
 	 	 r=l+len-1;
 	 	 for(int p=0;p<=n-r;p++)
 	 	 {
 	 	 	 dp[l][r][p]=max(dp[l][r][p],dp[l][r-1][0]+f[p+1]);
 	 	 	 for(int k=l;k<r;k++) if(c[k]==c[r])
 	 	 	 	 dp[l][r][p]=max(dp[l][r][p],dp[l][k][p+1]+dp[k+1][r-1][0]);
		 }
	 }
	 printf("%lld\n",dp[1][n][0]);
	 return 0;
}

UVA10559 方块消除 Blocks

$\texttt{solution}$

增加了字符集大小,使得例题的方法无法解决,只能用变式 \(1\) 的转移方法。

$\texttt{code}$
#define Maxn 205
typedef long long ll;
int n;
char s[Maxn];
int c[Maxn];
ll dp[Maxn][Maxn][Maxn];
// [l,r] 右边还有 k 个和 r 相同颜色的块 
int main()
{
	 int T=rd();
	 for(int Case=1;Case<=T;Case++)
	 {
		 n=rd();
		 memset(dp,0,sizeof(dp));
		 for(int i=1;i<=n;i++) c[i]=rd();
		 for(int len=1;len<=n;len++) for(int l=1,r;l<=n-len+1;l++)
	 	 {
	 	 	 r=l+len-1;
 	 	 	 for(int p=0;p<=n-r;p++)
			 {
			 	 dp[l][r][p]=maxll(dp[l][r][p],dp[l][r-1][0]+1ll*(p+1)*(p+1));
			 	 for(int k=l;k<r;k++) if(c[k]==c[r])
 	 	 	 	 	 dp[l][r][p]=maxll(dp[l][r][p],dp[k+1][r-1][0]+dp[l][k][p+1]);
			 }
		 }
		 printf("Case %d: %lld\n",Case,dp[1][n][0]);
	 }
	 return 0;
}

P3736 [HAOI2016]字符合并

$\texttt{solution}$

发现 \(k\) 比较小,考虑把 \(216\) 种状态都记录下来。

考虑区间 DP,在每段区间中只保留 \(\le k\) 个字符?好像是对的?

\(dp(l,r,S)\) 表示消除完成区间 \([l,r]\) 的字符后,这段区间剩余字符为 \(S(S<2^8)\) 的最大收益。

那么枚举状态要 \(\mathcal{O(2^8n^2)}\),枚举转移点要 \(\mathcal{O(2^8\times 8n)}\)

太太太太慢了啊啊啊,不会了捏。

\(\bigstar\texttt{Hint}\):状态非常正确,但是转移太辣鸡了。其实在枚举中间转移点后,归类到一下两种情况:

  • 左右区间没有合在一起消去的情况,直接钦定右区间剩余长度为 \(1\)(可以只枚举右区间长度为 \((k-1)\times p+1\) 的区间),左区间为其他的。
  • 左右区间合在一起,发现其实并不用特判,只用在合并后长度为 \(k\) 后直接合并一次即可。

发现这样可以覆盖完成所有情况,这道题的关键还是想到将不用的转移合并掉。

P4766 [CERC2014]Outer space invaders

$\texttt{solution}$

\(\bigstar\texttt{Hint}\):区间 DP 转移一般可以考虑最后一次进行的操作,然后将序列分为两个部分计算。

我们将所有外星人按照出现时间排序,离散化后在时间轴上考虑区间 DP。

我们发现如果需要消去一个区间,区间中距离最远一定需要被消去一次。

那只要随便找出一个最远的距离且出现、攻击时间 \([s,t]\subseteq [l,r]\) 的外星人。在它出现、攻击之间进行一次消去操作即可,转移方程如下:

\[dp_{l,r}=\min_{k\in[s,t]}\{dp_{l,k-1}+Dist_{id}+dp_{k+1,r}\} \]

CF643C Levels and Regions

$\texttt{solution}$

首先如果给出一个关卡通过的概率那么它的期望时间就是 \(\frac{1}{p_i}\),然而这样一段区间内的时间就是:

\[\sum_{i=l}^{r}\dfrac{1}{k_i}\left(\sum_{j=l}^{i}k_j\right) \]

发现每次计算一段区间内的时间非常麻烦,正难则反,考虑总时间减去不必要的部分。

上面总时间的定义为整个 \(n\) 作为整体,不划分,不必要的部分指:

\[\sum_{i=l}^{r}\dfrac{1}{k_i}\left(\sum_{j=1}^{i-1}k_j\right) \]

\(sk_{n}=\sum_{i=1}^{n}k_i\),同时令 \(gk_{n}=\sum_{i=1}^{n}\frac{1}{k_i}\)

如果我们令 \(dp_{i,k}\) 表示将前 \(i\) 个数划分为 \(k\) 段的最小时间,那么可以写出这样的转移方程:

\[dp_{i,k}=\min_{j\in[1,i-1]}\{dp_{j,k-1}-sk_{j}\times (gk_i-gk_j)\} \]

我们对它进行斜率优化,则:

\[dp_{i,k}=dp_{j,k-1}-sk_j\times gk_i+sk_j\times gk_j\\ dp_{j,k-1}+sk_j\times gk_j=sk_j\times gk_i+dp_{i,k}\\ [y]=[x]\times [k]+[b] \]

我们发现 \(k_i\) 是递增的且始终大于 \(0\),所以显然可以单调队列维护转移点。

实现的时候总时间最后再加也不迟。

CF1187F Expected Square Beauty

$\texttt{solution}$

\(I_{i}=[a_{i}=a_{i+1}]\),则其实 \(B(x)=\sum_+{i=1}^{n-1}I_i\)

那么如果要求平方的期望(注意平方的期望和期望的平方不同),可以进行如下推导:

\[\begin{aligned} ans&=E(B(x)^2)\\ &=E\left(\sum_{i=1}^{n-1}I_i\sum_{j=1}^{n-1}I_j\right)\\ &=\sum_{i=1}^{n-1}\sum_{j=1}^{n-1}E(I_iI_j)\\ \end{aligned} \]

\(E(I_iI_j)\) 表示为 \(i\)\(j\) 都为 \(1\) 的概率,下面对 \(E(I_iI_j)\) 分类讨论计算:

  • 如果 \(|i-j|>1\),则答案为 \(E(a_i\not =a_{i+1})\times E(a_j\not =a_{j+1})\)

  • 如果 \(i=j\),则答案为 \(E(a_i\not =a_{i+1})\)

  • 如果 \(|i-j|=1\),发现了 \(i+1=j\),即概率之间不独立,需要计算 \(E((a_i\not =a_{i+1})\&(a_{i+1}\not =a_{i+2}))\),化简可得,上面的式子为:

    \[1-E(a_i=a_{i+1})-E(a_{i+1}=a_{i+2})+E(a_{i}=a_{i+1}=a_{i+2}) \]

之后可以 \(\mathcal{O(n)}\) 推啦!

P5369 [PKUSC2018]最大前缀和

$\texttt{solution}$

以后看到这种 \(n\le 20\) 的题应该想到状压。

首先找找最大前缀和的一些性质,记一个排列 \(P\) 的最大前缀为 \([1,k]\),有如下性质:

  • 子序列 \([1,k]\) 不存在任何一段真后缀(即不等于整个区间)的和 \(<0\)(因为可能全都是负数)。
  • 子序列 \([k+1,n]\) 不存在任何一段前缀的和 \(\ge 0\)

(上面一个 \(\le\) 一个 \(>\) 为了去重)

这样启发我们将前后缀分开来计算,那么使用状压 DP。

  • \(f_s\) 表示将集合 \(s\) 中的数排成序列后不存在任何一段真后缀的和 \(\le 0\) 的方案数;
  • \(g_s\) 表示将集合 \(s\) 中的数排成序列后不存在任何一段前缀的和 \(>0\) 的方案数。

那么答案就是 \(\sum_{s}f_s\times g_{U-s}\times sum_s\),考虑 \(f\) 从后往前加数,\(g\) 从前往后加数,且 \(f,g\) 有如下转移:

\[f_{s|x}\leftarrow [sum_{s}\ge 0]\times f_s\\ g_{s|x}\leftarrow [sum_{s|x}< 0]\times g_s \]

posted @ 2021-07-27 17:37  EricQian06  阅读(53)  评论(0编辑  收藏  举报