动态规划的各类模型<二>

上文:\(link\) :动态规划的各种模型<一>

本篇主要讲解状压 DP ,区间 DP ,数位 DP。

配合文章右上角目录食用更佳。


状态压缩

状态压缩类的DP常见的有两种:

  • 基于连通性的DP,也就是棋盘式DP。
  • 集合式的DP,大概就是说这个元素是不是在这个集合里面。

本章中的按位运算使用的是计算机语言中的符号。

国王

\(n×n\) 的棋盘上放 \(k\) 个国王,国王可攻击相邻的 \(8\) 个格子,求使它们无法互相攻击的方案总数。

输入格式

共一行,包含两个整数 \(n\)\(k\)

输出格式

共一行,表示方案总数,若不能够放置则输出 \(0\)

数据范围

\(1≤n≤10,\ 0≤k≤n^2\)

输入样例:

3 2

输出样例:

16

解析

数据范围很小,但是爆搜必死。

按行枚举,我们发现,当我们在试图摆第 \(i\) 行的棋子时,这一行哪些位置能摆完全取决于上一行的摆法,与上上行完全没有关系。这就提示我们只关心上一行怎么摆。我们可以将每一行表示成一个 01 串,0 表示这个位置没摆国王;1 表示这个位置摆了国王。可以将这个 01 串看做二进制表示,将其压缩成一个十进制数。

仍然从两个角度考虑:

  • 状态设计:

    \(f(i,j,state)\) 表示确定完了前 \(i\) 行,用了 \(j\) 个棋子,第 \(i\) 行的状态是 \(state\) 的所有方案数。

现在我们来看这个集合表示了什么。

寻找最后一不同点。由于第 \(i\) 行已经确定,我们需要枚举第 \(i-1\) 行的所有状态,就有 \(2^n\) 种情况。现在我们要从里面筛选出合法的状态来转移到当前状态。什么状态是合法的?首先,相邻两个格子不应当同时放,并且第 \(i-1\) 行与第 \(i\) 行不能有同一位置或相邻位置的国王,即:

  1. \(a\&b=0\)
  2. \(a,\ b,\ (a|b)\) 不能有相邻的 \(1\)

只要满足以上两个条件,我们的状态就是合法的。也就是说,已经摆完前 \(i\) 行且第 \(i\) 行状态为 \(a\)\(i-1\) 行状态为 \(b\) 且已经摆了 \(j\) 个国王的所有方案都可以由已经摆完了前 \(i-1\) 行,并且第 \(i-1\) 行状态为 \(b\) 且摆了 \(j-count(a)\) 个国王的所有方案转移过去。也就是 \(f(i-1,j-conut(a),b)\)

这里我们可以将判定条件以及状态的转移预处理出来。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=15;

int n,m;
ll f[N][N*N][1<<(N-4)];
vector<int> state;//合法状态
int id[1<<(N-4)],cnt[1<<(N-4)],tot=0;
vector<int> head[1<<(N-4)];//状态转移

int lowbit(int x) {return x&(-x);}

int count(int x)
{
	int res=0;
	while(x) x-=lowbit(x),res++;
	return res;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<(1<<n);i++)//预处理合法状态
	{
		if(i&(i>>1)) continue;
		state.push_back(i);
		id[i]=state.size()-1;
		cnt[i]=count(i);
	}
	for(int i=0;i<state.size();++i)//预处理状态转移关系
	{
		for(int j=0;j<state.size();++j)
		{
			int a=state[i],b=state[j];
			if(((a&b)==0) && ((a|b)&((a|b)>>1))==0) head[i].push_back(j);
		}
	}
	f[0][0][0]=1;
	for(int i=1;i<=n+1;i++)//我们算到n+1行,这样能够方便统计答案
	{
		for(int j=0;j<=m;j++)
		{
			for(int a=0;a<state.size();a++)
			{
				for(int b=0;b<head[a].size();b++)
				{
					int t=head[a][b];
					int c=cnt[state[a]];
					if(j>=c) f[i][j][a]+=f[i-1][j-c][t];
				}
			}
		}
	}
	printf("%lld",f[n+1][m][0]);
    return 0;
}

玉米田

农夫约翰的土地由 \(M×N\) 个小方格组成,现在他要在土地里种植玉米。

非常遗憾,部分土地是不育的,无法种植。

而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。

现在给定土地的大小,请你求出共有多少种种植方法。

土地上什么都不种也算一种方法。

输入格式

\(1\) 行包含两个整数 \(M\)\(N\)

\(2\dots M+1\) 行:每行包含 \(N\) 个整数 \(0\)\(1\),用来描述整个土地的状况,\(1\) 表示该块土地肥沃,\(0\) 表示该块土地不育。

输出格式

输出总种植方法对 \(10^8\) 取模后的值。

数据范围

\(1≤M,N≤12\)

输入样例:

2 3
1 1 1
0 1 0

输出样例:

9

解析

抽象题意:棋盘式DP,棋子之间不能有公共边,有些地方不能放棋子,求方案数。

当前行的状态仍然只能被上一行影响到。

  • 状态表示:
    \(f(i,st)\) 为摆了前 \(i\) 行且第 \(i\) 行状态为 \(st\) 的所有方案数。

对于状态计算,我们仍然是找最后一个不同点。最后一行的状态都是一样的,我们要根据倒数第二行的状态来分类。我们要从上一行的状态中找到能够正确转移到当前状态的状态。

假设第 \(i\) 行的状态为 \(a\),第 \(i-1\) 行状态为 \(b\)。由于这里的限制是不能有公共边,所以合法的状态转移要满足的条件是:

  1. \(a\&b=0\)
  2. \(a,b\) 不能有相邻的 \(1\)

只要 \(b\) 能满足上述条件,那么 \(f(i-1,b)\) 就能合法地转移到 \(f(i,a)\) 。预处理方式参考上面。

#include <bits/stdc++.h>
using namespace std;

const int N=15,M=1<<12+2,mod=1e8;

int n,m;
int is[N],f[N][M];
vector<int> vec,head[M];

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<m;j++)
		{
			int x;
			scanf("%d",&x);
			is[i]+=(!x)*(1<<j);
		}
	}

	for(int i=0;i<(1<<m);i++)
		if(!(i&(i>>1))) vec.push_back(i);
	for(int i=0;i<vec.size();i++)
	{
		for(int j=0;j<vec.size();j++)
		{
			int a=vec[i],b=vec[j];
			if(!(a&b)) head[i].push_back(j);
		}
	}

	f[0][0]=1;
	for(int i=1;i<=n+1;i++)
	{
		for(int j=0;j<vec.size();j++)
		{
			if(!(vec[j]&is[i]))
				for(int k:head[j])
					f[i][j]=(f[i][j]+f[i-1][k])%mod;
		}
	}
	printf("%d",f[n+1][0]);
	return 0;
}


炮兵阵地

司令部的将军们打算在 \(N×M\) 的网格地图上部署他们的炮兵部队。

一个 \(N×M\) 的地图由 \(N\)\(M\) 列组成,地图的每一格可能是山地(用 \(H\) 表示),也可能是平原(用 \(P\) 表示),如下图。

在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

1185_1

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。

图上其它白色网格均攻击不到。

从图上可见炮兵的攻击范围不受地形的影响。

现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入格式

第一行包含两个由空格分割开的正整数,分别表示 \(N\)\(M\)

接下来的 \(N\) 行,每一行含有连续的 \(M\) 个字符( \(P\) 或者 \(H\) ),中间没有空格。按顺序表示地图中每一行的数据。

输出格式

仅一行,包含一个整数 \(K\),表示最多能摆放的炮兵部队的数量。

数据范围

\(N≤100,M≤10\)

输入样例:

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

输出样例:

6

解析

还是棋盘式。

此时我们的状态涉及到了前两行,所以我们要关心前两行的状态。

  • 状态设计:设 \(f(i,j,k)\) 为摆到第 \(i\) 行,第 \(i-1\) 行状态为 \(j\),第 \(i-2\) 行状态为 \(k\) 的最大方案。

思考限制条件,一个棋子附近横竖两格都不能放置棋子,并且有一些格子不能放置棋子,假设第 \(i\) 行状态是 \(a\) ,第 \(i-1\) 行状态是 \(b\),第 \(i-2\) 行状态是 \(c\),第 \(i\) 行各个格子的可用情况为 \(g[i]\) ( \(0\) 为可放 \(1\) 为不可放),那么:

  1. \((a\&b)|(a\&c)|(b\&c)=0\)
  2. \((a\& g[i])|(b\& g[i-1])|(c\& g[i-2])=0\)
  3. 相邻两个 \(1\) 之间至少隔两个 \(0\)

算算时间复杂度 \(O(n2^{3m})\) 看起来过不了。

但是实际上在本题的强限制下,有效状态少得多。我们将状态转移预处理即可。

#include <bits/stdc++.h>
using namespace std;

const int N=15,M=(1<<10)+10;

int n,m;
int g[110];
vector<int> vec;//所有的合法状态
int f[3][M][M];

bool check(int sta)
{
	for(int i=0;i<m;i++)
		if((sta>>i&1) && ((sta>>(i+1)&1)||(sta>>(i+2)&1)))
		return 0;
	return 1;
}

int lowbit(int x) {return x&(-x);}

int count(int x)
{
	int ans=0;
	while(x) x-=lowbit(x),ans++;
	return ans;
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<m;j++)
		{
			char ch;
			cin>>ch;
			if(ch=='H') g[i]+=(1<<j);
		}
	}
	for(int i=0;i<(1<<m);i++)
		if(check(i)) vec.push_back(i);
	for(int i=1;i<=n+2;i++)
	{
		for(int j=0;j<vec.size();j++)//当前行
		{
			for(int k=0;k<vec.size();k++)//上一行
			{
				for(int u=0;u<vec.size();u++)//上两行
				{
					int a=vec[j],b=vec[k],c=vec[u];
					if((a&b)|(b&c)|(a&c)) continue;
					if((g[i]&a)|(g[i-1]&b)) continue;
					f[i&1][j][k]=max(f[i&1][j][k],f[(i-1)&1][k][u]+count(a));
				}
			}
		}
	}
	cout<<f[(n+2)&1][0][0];
	return 0;
}


『NOIp提高组2017』愤怒的小鸟

Kiana 最近沉迷于一款神奇的游戏无法自拔。

简单来说,这款游戏是在一个平面上进行的。

有一架弹弓位于 \((0,0)\) 处,每次 Kiana 可以用它向第一象限发射一只红色的小鸟, 小鸟们的飞行轨迹均为形如 \(y=ax^2+bx\) 的曲线,其中 \(a,b\) 是 Kiana 指定的参数,且必须满足 \(a<0\)

当小鸟落回地面(即 x 轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有 \(n\) 只绿色的小猪,其中第 \(i\) 只小猪所在的坐标为 \((x_i,y_i)\)

如果某只小鸟的飞行轨迹经过了 \((x_i, y_i)\),那么第 \(i\) 只小猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;

如果一只小鸟的飞行轨迹没有经过 \((x_i, y_i)\),那么这只小鸟飞行的全过程就不会对第 \(i\) 只小猪产生任何影响。

例如,若两只小猪分别位于 \((1,3)\) 和 \((3,3)\),Kiana 可以选择发射一只飞行轨迹为 \(y=−x^2+4x\) 的小鸟,这样两只小猪就会被这只小鸟一起消灭。

而这个游戏的目的,就是通过发射小鸟消灭所有的小猪。

这款神奇游戏的每个关卡对 Kiana 来说都很难,所以 Kiana 还输入了一些神秘的指令,使得自己能更轻松地完成这个这个游戏。

这些指令将在输入格式中详述。

假设这款游戏一共有 \(T\) 个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的小猪。

由于她不会算,所以希望由你告诉她。

输入格式

第一行包含一个正整数 \(T\),表示游戏的关卡总数。

下面依次输入这 \(T\) 个关卡的信息。

每个关卡第一行包含两个非负整数 \(n,m\),分别表示该关卡中的小猪数量和 Kiana 输入的神秘指令类型。

接下来的 \(n\) 行中,第 \(i\) 行包含两个正实数 \((x_i,y_i)\),表示第 \(i\) 只小猪坐标为 \((x_i,y_i)\),数据保证同一个关卡中不存在两只坐标完全相同的小猪。

  • 如果 \(m=0\),表示 Kiana 输入了一个没有任何作用的指令。

  • 如果 \(m=1\),则这个关卡将会满足:至多用 \(⌈n/3+1⌉\) 只小鸟即可消灭所有小猪。

  • 如果 \(m=2\),则这个关卡将会满足:一定存在一种最优解,其中有一只小鸟消灭了至少 \(⌊n/3⌋\) 只小猪。

保证 \(1≤n≤18,0≤m≤2,0<x_i,y_i<10\),输入中的实数均保留到小数点后两位。

输出格式

对每个关卡依次输出一行答案。

输出的每一行包含一个正整数,表示相应的关卡中,消灭所有小猪最少需要的小鸟数量。

输入样例1

2
2 0
1.00 3.00
3.00 3.00
5 2
1.00 5.00
2.00 8.00
3.00 9.00
4.00 8.00
5.00 5.00

输出样例1

1
1

输入样例2

3
2 0
1.41 2.00
1.73 3.00
3 0
1.11 1.41
2.34 1.79
2.98 1.49
5 0
2.72 2.72
2.72 3.14
3.14 2.72
3.14 3.14
5.00 5.00

输出样例2

2
2
3

解析

首先,由于题中的抛物线的特殊性,我们只需要两个点就可以确定一个抛物线。当然,这两个点横坐标不能相等。

这样的话,我们最多有 \(n^2\) 条抛物线。\(n\) 很小,所以我们可以直接预处理出来。顺便我们还可以算一下哪些点在这些抛物线上。

那么问题就变成了选择一些抛物线所有的点全部覆盖完,最少选择多少抛物线。这是一个重复覆盖问题。

似乎是可以用 Dancing Links 做的,但是我不会用

我们可以设状态 \(f(state)\) 代表当前被覆盖点的情况为 \(state\) 的时候我们所取得的最少抛物线数量。

由于思考它的来源并不好做,我们想想它能够去更新哪些状态。(这就是一个刷表法和填表法的取舍问题)

对于当前的状态,我们贪心的选择两个没被覆盖的点,得到他们能够覆盖的点 \(path(i,j)\) 。(当然,压缩成了一个整数) 此时,\(state\) 能更新到的状态 \(state^{\prime}=path(i,j)|state\)

接下来我们来解决一个数学问题:给两个点 \((x_1,y_1),\ (x_2,y_2)\),确定一条形如 \(y=ax^2+bx\) 的抛物线。

联立方程组:

\[\begin{cases} ax_1^2+bx_1=y_1\\ \\ ax_2^2+bx_2=y_2 \end{cases} \]

有题目条件 \(0<x_i,y_i<10\) ,我们可以直接等式两边同除 \(x_1/x_2\),得到:

\[\begin{cases} ax_1+b=\frac{y_1}{x_1}\\ \\ ax_2+b=\frac{y_2}{x_2} \end{cases} \]

然后等式相减:

\[(x_1-x_2)a=\frac{x_1}{y_1}-\frac{x_2}{y_2}\\\ \\ \Rightarrow a=\frac{\frac{x_1}{y_1}-\frac{x_2}{y_2}}{x_1-x_2} \]

将式子代入,得到:

\[b=\frac{y_1}{x_1}-ax_1 \]

#include <bits/stdc++.h>
using namespace std;
typedef pair<double,double> PDD;
#define x first
#define y second
const int N=18+2;
const double eps=1e-6;

int n,m;
int f[1<<N];
PDD q[N];
int path[N][N];

bool cmp(double a,double b)
{
	if(fabs(a-b)<=eps) return 0;
	if(a<b) return -1;
	return 1;
}

int main()
{
	int T;
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(int i=0;i<n;i++)
			scanf("%lf%lf",&q[i].x,&q[i].y);
		memset(path,0,sizeof path);
		for(int i=0;i<n;i++)
		{
			path[i][i]=(1<<i);
			for(int j=0;j<n;j++)
			{
				double x1=q[i].x, x2=q[j].x;
				double y1=q[i].y, y2=q[j].y;
				if(!cmp(x1,x2)) continue; //它们的坐标相等。
				double a=(x2*y1-x1*y2)/(x1*x1*x2-x2*x2*x1);
				double b=y1/x1-a*x1;
				if(a>-eps) continue;
				int state=0;
				for(int k=0;k<n;k++)
				{
					double xx=q[k].x, yy=q[k].y;
					if(!cmp(a*xx*xx+b*xx,yy)) state|=(1<<k);
				}
				path[i][j]=state;
			}
		}

		memset(f,0x3f,sizeof f);
		f[0]=0;
		for(int i=0;i<(1<<n);i++)
		{
			int x=0;
			for(int j=0;j<n;j++)
				if(!(i>>j&1))
				{
					x=j;
					break;
				}
			for(int j=0;j<n;j++)
				f[i|path[x][j]]=min(f[i|path[x][j]],f[i]+1);
		}
		printf("%d\n",f[(1<<n)-1]);
	}
	return 0;
}


区间DP

区间 DP 就是在区间中进行 DP ,求解一段区间上的最优解。主要是通过合并小区间的最优解进而得出整个大区间上最优解的 DP 。

环形石子合并

\(n\) 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数 \(n\) 及每堆的石子数,并进行如下计算:

  1. 选择一种合并石子的方案,使得做 \(n−1\) 次合并得分总和最大。
  2. 选择一种合并石子的方案,使得做 \(n−1\) 次合并得分总和最小。

输入格式

第一行包含整数 \(n\),表示共有 \(n\) 堆石子。

第二行包含 \(n\) 个整数,分别表示每堆石子的数量。

输出格式

输出共两行:

第一行为合并得分总和最小值,

第二行为合并得分总和最大值。

数据范围

\(1≤n≤200\)

输入样例:

4
4 5 9 4

输出样例:

43
54

解析

石子合并的扩展。

对于环形,我们有一个非常非常 常见的技巧:破环为链。

什么是破环为链?我们将链从一个地方断开,得到一个序列,然后再将这条链复制一遍接在后面。这样,我们所有在环中可能出现的区间就全部存在了。

设状态 \(f(i,j)\) 表示所有将区间 \([i,j]\) 内全部石子全部合并的方案得到分数的最大/小值。

我们用不同方法把 \([i,j]\) 划分成两个集合,得到数个左右端点分别为 \(i,j\) 的子区间对 \(([i,k],[k,j])\)。仍然找最后一个不同点。这些方案的不同就是区间的分界点不同。我们将子区间对两边的区间答案合并起来即可。

也就是说 \(f(i,j)=min\{f(i,k)+f(k+1,j)+str(i,j)\}\) 其中 \(str(i,j)\) 是区间 \([i,j]\) 中石子的总数。

最后统计答案的时候,我们合并的区间长度最大是 \(n\) ,所以要枚举所有长度为 \(n\) 的区间取最值。

这类一维区间上的 DP 一般都使用迭代的方式,迭代循环也基本固定:

for(int len=1;len<=n;len++)//枚举长度
{
    for(int l=1;l+n-1<=2*n;l++)//枚举左端点
    {
        int r=l+len-1;
        /*do something*/
    }
}

那么本题的代码也呼之欲出:

#include <bits/stdc++.h>
using namespace std;

const int N=410;

int n;
int str[N];
int f[N][N],g[N][N];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&str[i]),str[n+i]=str[i];
	for(int i=1;i<=n*2;i++) str[i]+=str[i-1];

	memset(f,0x3f,sizeof f);
	for(int len=1;len<=n;len++)
	{
		for(int l=1;l+len-1<n*2;l++)
		{
			int r=l+len-1;
			if(len==1) f[l][r]=g[l][r]=0;
			else{
				for(int k=l;k<r;k++)
				{
					f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+str[r]-str[l-1]);
					g[l][r]=max(g[l][r],g[l][k]+g[k+1][r]+str[r]-str[l-1]);
				}
			}
		}
	}
	int ans1=0,ans2=1e8;
	for(int i=1;i<=n;i++)
	{
		ans1=max(ans1,g[i][i+n-1]);
		ans2=min(ans2,f[i][i+n-1]);
	}
	printf("%d\n%d",ans2,ans1);
	return 0;
}


『NOIp提高组2006』能量项链

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。

能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。

并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。

因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。

如果前一颗能量珠的头标记为 \(m\),尾标记为 \(r\),后一颗能量珠的头标记为 \(r\),尾标记为 \(n\),则聚合后释放的能量为 \(m×r×n\)(Mars 单位),新产生的珠子的头标记为 \(m\),尾标记为 \(n\)

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。

显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 \(N=4\)\(4\) 颗珠子的头标记与尾标记依次为 \((2,3)(3,5)(5,10)(10,2)\)

我们用记号 \(⊕\) 表示两颗珠子的聚合操作,\((j⊕k)\) 表示第 \(j,k\) 两颗珠子聚合后所释放的能量。则

\(4,1\) 两颗珠子聚合后释放的能量为:\((4⊕1)=10×2×3=60\)

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为 \(((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710\)

输入格式

输入的第一行是一个正整数 \(N\),表示项链上珠子的个数。

第二行是 \(N\) 个用空格隔开的正整数,所有的数均不超过 \(1000\),第 \(i\) 个数为第 \(i\) 颗珠子的头标记,当 \(i<N\) 时,第 \(i\) 颗珠子的尾标记应该等于第 \(i+1\) 颗珠子的头标记,第 \(N\) 颗珠子的尾标记应该等于第 \(1\) 颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式

输出只有一行,是一个正整数 \(E\),为一个最优聚合顺序所释放的总能量。

数据范围

\(4≤N≤100, 1≤E≤2.1×10^9\)

输入样例

4
2 3 5 10

输出样例

710

解析

读题,总结题目信息得到:

题目给我们一个环,环中的元素由有序数对 \((m,n)\) 组成,可以将两个形如 \((m,k),(k,n)\) 的数对合并,合并运算 \(\oplus\) 定义为:\((m,k)\oplus (k,n)=(m,n)\) 并得到 \(m\times k\times n\) 的分数。让我们选择一个顺序,合并所有的珠子分数最大。

仔细观察一下这个运算形式 \(\cdots\cdots\) 这不就矩阵乘法?

长宽分别为 \(m,k\) 和长宽分别为 \(k,n\) 的矩阵相乘,得到的就是长宽为 \(m,n\) 的矩阵,且其运算次数刚好就是 \(m\times k\times n\)

不管怎么说,这是一个环形 DP 问题,我们先从线性出发考虑。

对线性的情况进行分析。

我们先套路地试着假设状态,若不对再来调整。

  • 状态 \(f(i,j)\) 表示:合并区间 \([l,r]\) 中的所有珠子的所有方案能得到的最大值。

通过对珠子合并的模拟我们发现,在题中给的条件下,将相邻的珠子合并,合并出来的珠子一定能和前后的珠子合并。也就是说,我们除了合并相邻珠子外没有其他判定条件。

现在我们进行集合划分。

对于一个 \(f(L,R)\) ,我们将 \([L,R]\) 合并成一个珠子,最后一步必定是将两个珠子合并为一个,这两个珠子由两个精确覆盖 \([L,R]\) 的子区间 \([L,k],[k+1,R]\) 合并而来。若我们要计算 \(f(L,R)\) ,那么我们就可以枚举所有的合并方案,也就是分界点 \(k\) 的位置,取其中的较大值。

据此,我们得出状态转移方程: \(f(L,R)=max\{f(L,k)+f(k+1,R)+energy(L,k,R)\},\ (k\in [L,R))\)

接下来我们处理 \(energy\) 函数。

分析题目给出的序列 \(w_i\)\(w_1\)\(1\) 号珠子的前缀,是 \(n\) 号珠子的后缀;\(w_2\)\(2\) 号珠子的前缀,是 \(1\) 号珠子的后缀 …… 而由上面合并运算的定义,区间 \([L,R]\) 的珠子合并后,前缀是 \(L\) 的前缀,后缀是 \(R\) 的后缀。我们得到 \(energy(L,k,R)\) 的计算式:\(energy(L,k,R)=w_L\times w_{k+1}\times w_{R+1}\)

最后,我们处理环形问题。

仍然破环为链,再将链复制一遍接到后面。最后枚举区间长度求最值。

#include <bits/stdc++.h>
using namespace std;

const int N=510;

int n;
int w[N];
int f[N][N];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&w[i]),w[i+2*n]=w[i+n]=w[i];
	memset(f,-0x3f,sizeof f);
	for(int len=1;len<=n;len++)
	{
		for(int l=1;l+len-1<2*n;l++)
		{
			int r=l+len-1;
			if(len==1) f[l][r]=0;
			for(int k=l;k<r;k++)
				f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+w[l]*w[k+1]*w[r+1]);
		}
	}
	int ans=-1145141919;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i][i+n-1]);
	printf("%d",ans);
	return 0;
}


凸多边形的划分

给定一个具有 \(N\) 个顶点的凸多边形,将顶点从 \(1\)\(N\) 标号,每个顶点的权值都是一个正整数。

将这个凸多边形划分成 \(N−2\) 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。

输入格式

第一行包含整数 \(N\),表示顶点数量。

第二行包含 \(N\) 个整数,依次为顶点 \(1\) 至顶点 \(N\) 的权值。

输出格式

输出仅一行,为所有三角形的顶点权值乘积之和的最小值。

数据范围

\(N≤50\)
数据保证所有顶点的权值都小于 \(10^9\)

输入样例:

5
121 122 123 245 231

输出样例:

12214884

解析

我们画个图:

这是一种可能的情况。总之,每条边在最后都会属于一个三角形。

我们来看更加复杂的划分过程。

在上图中,我们选定一条边 \((1,8)\) 和一个点 \((5)\),连接起来。

对于这个情况,我们发现,这个凸多边形被分成了两个独立的凸多边形和一个三角形。

三角形的价值可以算出来,而两边的凸多边形又可以继续细分。这似乎有些最优子结构的影子。

我们设 \(f(i,j)\) 是划分 由边 \((i,i+1),(i+1,i+2)\cdots (j-1,j),(j,i)\) 组成的凸多边形 得到的最小价值。

再根据上图,当我们向这样划分的时候,总的最小值就是左边多边形的最小价值与右边最小价值的和加上三角形的价值,也就是 \(f(1,8)=f(1,5)+f(5,8)+w_1\times w_5\times w_8\)

以此类推,我们可以得到状态转移方程:

\[f(i,j)=\min\{f(i,k)+f(k,j)+w_i\times w_k\times w_j\}\qquad (k\in (i,j)\ ) \]

注意此题要高精(为了节省篇幅代码里面没加)

code:

#include <bits/stdc++.h>
using namespace std;
const int N=100,INF=1e8+10;
int n;
int f[N][N];
int w[N];
int main()
{
	ios::sync_with_stdio(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int len=3;len<=n;len++)
	{
		for(int l=1;l+len-1<=n;l++)
		{
			int r=l+len-1;
			f[l][r]=INF;
			for(int k=l+1;k<r;k++)
				f[l][r]=min(f[l][r],f[l][k]+f[k][r]+w[l]*w[k]*w[r]);
		}
	}
	cout<<f[1][n];
	return 0;
}

『NOIp提高组2003』 加分二叉树

设一个 \(n\) 个节点的二叉树 \(\text{tree}\) 的中序遍历为(\(1,2,3,…,n\)),其中数字 \(1,2,3,…,n\) 为节点编号。

每个节点都有一个分数(均为正整数),记第 \(i\) 个节点的分数为 \(d_i\)\(\text{tree}\) 及它的每个子树都有一个加分,任一棵子树 \(\text{subtree}\)(也包含 \(\text{tree}\) 本身)的加分计算方法如下:

\(\text{subtree}\) 的左子树的加分 \(×\ \text{subtree}\) 的右子树的加分 \(+\) \(\text{subtree}\) 的根的分数

若某个子树为空,规定其加分为 \(1\)

叶子的加分就是叶节点本身的分数,不考虑它的空子树。

试求一棵符合中序遍历为(\(1,2,3,…,n\))且加分最高的二叉树 \(\text{tree}\)

要求输出:

(1)\(\text{tree}\) 的最高加分

(2)\(\text{tree}\) 的前序遍历

输入格式

\(1\) 行:一个整数 \(n\),为节点个数。

\(2\) 行:\(n\) 个用空格隔开的整数,为每个节点的分数(\(0<\) 分数 \(<100\) )。

输出格式

\(1\) 行:一个整数,为最高加分(结果不会超过 int 范围)。

\(2\) 行:\(n\) 个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。

数据范围

\(n<30\)

输入样例:

5
5 7 1 2 10

输出样例:

145
3 1 2 4 5

解析

先来看看树的中序遍历是怎么样的。

\(1\) 号节点的左右子树分别是橙色的两段,\(2\) 号节点的左右子树分别是绿色的两段,\(3\) 号节点的左右子树分别是紫色的两段。

由于它是一个二叉树,每个节点最多会把当前所在子树的区间分成两部分(加上自己三部分)。

我们发现,这似乎是一个区间合并问题。每次我们将子树合并成一个更大的子树的过程就是中序序列两个区间合并的过程。

  • 设状态 \(f(i,j)\) 为所有由 \([i,j]\) 构造出的树能获得的最大价值。

  • 状态计算:

    对于确定的区间 \([l,r]\),我们不知道区间内哪个点是根节点,所以我们枚举一个分界点 \(k\) 作为当前区间代表子树的根节点。

    画出区间对应的树我们发现,它的左子树,右子树和它自己,这是三个独立的部分,只要三个都取最大值就能得到全局最大值:

    也就是说这是一个具有最优子结构性质的东西。那么我们的状态转移方程也出来了:

    \[f(i,j)=max\{f(i,k-1)\times f(k+1,j)+w_k\}\ ,\qquad k\in(i,j) \]

    这里我们不用讨论子树空集的情况,因为比起空集总有情况更优。

以上就是 DP 部分,我们现在要思考如何记录并输出方案。

记录方案的过程就是记录 DP 决策的过程。我们在 max 的时候记录每个子区间选到了哪一个分界点,要输出方案的时候从总区间开始递归往下输出即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=50;

int n;
ll f[N][N],path[N][N];
int w[N];

void dfs(int l,int r)//输出方案
{
	int k=path[l][r];
	printf("%lld ",path[l][r]);
	if(path[l][k-1]) dfs(l,k-1);
	if(path[k+1][r]) dfs(k+1,r);
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);

	for(int len=1;len<=n;len++)
	{
		for(int l=1;l+len-1<=n;l++)
		{
			int r=l+len-1;
			if(len==1) f[l][r]=w[l],path[l][r]=l;
			else if(len==2) f[l][r]=w[l]+w[r],path[l][r]=l;//特判
			else {
				for(int k=l+1;k<r;k++)
					if(f[l][r]<f[l][k-1]*f[k+1][r]+w[k])
					{
						f[l][r]=f[l][k-1]*f[k+1][r]+w[k];
						path[l][r]=k;
					}
			}
		}
	}
	printf("%lld\n",f[1][n]);
	dfs(1,n);
}

『NOI1999』棋盘分割

将一个 \(8×8\) 的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了 \((n−1)\) 次后,连同最后剩下的矩形棋盘共有 \(n\) 块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)

原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。

现在需要把棋盘按上述规则分割成 \(n\) 块矩形棋盘,并使各矩形棋盘总分的均方差最小。

均方差 \(\sigma=\sqrt{\frac{\sum_{i=1}^n(x_i-\overline{x})^2}{n}}\) ,其中平均值 \(\overline{x}=\frac{\sum_{i=1}^nx_i}{n}\)\(x_i\) 为第 \(i\) 块矩形棋盘的总分。

请编程对给出的棋盘及 \(n\),求出均方差的最小值。

输入格式

\(1\) 行为一个整数 \(n\)

\(2\) 行至第 \(9\) 行每行为 \(8\) 个小于 \(100\) 的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。

输出格式

输出最小均方差值(四舍五入精确到小数点后三位)。

数据范围

\(1<n<15\)

输入样例:

3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3

输出样例:

1.633

解析

先来看看我们要最小化的这个均方差(其实就是标准差) \(\sigma=\sqrt{\frac{\sum_{i=1}^n(x_i-\overline{x})^2}{n}}\)

显然我们要最小化这个东西就要最小化它的方差 \(S=\sigma^2=\frac{\sum_{i=1}^n(x_i-\overline{x})^2}{n}\)

然后推下柿子:

\[\begin{aligned} S&=\frac{\sum_{i=1}^n(x_i-\overline{x})^2}{n}\\ &=\frac{1}{n}\cdot \sum_{i=1}^n(x_i^2-2x_i\overline{x}+\overline{x}^2)\\ &=\frac{1}{n}\cdot(\sum_{i=1}^nx_i^2-2\overline{x}\sum_{i=1}^nx_i+n\overline{x}^2)\\ &=\frac{1}{n}\cdot(\sum_{i=1}^nx_i^2-2\overline{x}\cdot n\overline{x}+n\overline{x}^2)\\ &=\frac{1}{n}\cdot(\sum_{i=1}^nx_i^2-n\overline{x}^2)\\ &=\frac{\sum_{i=1}^nx_i^2}{n}-\overline{x}^2\\ \end{aligned} \]

最终的 \(\overline{x}\) 是固定的,我们的目标是最小化 \(\sum_{i=1}^nx_i^2\)

也就是说,我们要让每一部分的价值(即格子数)的平方和最小。

题目中给的方法是将一个大方格图分成几个方格图,这个过程是二维区间分割的过程。我们可以试着使用区间 DP 解决问题。

下面我们定义 \([a,b,c,d]\) 表示一个二维区间,它对应方格图中左上角在 \((a,b)\),右下角在 \((c,d)\) 的子矩阵。

  • 状态设计:设状态 \(f(a,b,c,d,k)\) 表示分割区间 \([a,b,c,d]\)\(k\) 块能得到的最大平方和。

  • 状态计算:
    考虑一个区间怎么分割。

    首先我们可以任意将这个区间一分为二,然后分别处理两个区间。

    对于上面这个特例,我们此时可以选择分割左区间或右边区间。

    • 若是选择分割右边区间,那么当前区间的答案 \(f(1,1,8,8,k)=f(3,1,8,8,k-1)+sum(1,1,3,8)^2\)
    • 若是选择分割左边区间,那么当前区间的答案 \(f(1,1,8,8,k)=f(1,1,3,8,k-1)+sum(3,1,8,8)^2\)

    两个答案取最小值即可。

    我们将上面的情况扩展,枚举分界点 \(t\),状态转移方程如下:

    \(f(l_1,r_1,l_2,r_2,k)=\min\{f(l_1,r_2,t,r_2,k-1)+sum(t+1,r_1,l_2,r_2)^2,f(t+1,r1,l2,r2)+sum(l_1,r_1,t,r2)^2\}\)

    这只是竖着割的情况,横着割也同理。

    \(f(l_1,r_1,l_2,r_2,k)=\min\{f(l_1,r_1,l_2,t)+sum(l_1,t+1,l_2,r_2)^2,f(l_1,t+1,l_2,t_2)+sum(l_1,r_1,l_2,t)^2\}\)

    • 注意,当 \(k=1\) 时,\(f(a,b,c,d,1)=sum(a,b,c,d)^2\)

DP 得到最小平方和之后整体代入到上面的式子算即可

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll N=30,INF=1e10;

int n;
ll a[N][N],s[N][N];
ll f[N][N][N][N][N];

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=8;i++)
	for(int j=1;j<=8;j++)
		scanf("%lld",&a[i][j]);
	for(int i=1;i<=8;i++)
	for(int j=1;j<=8;j++)
		s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
	for(int l1=1;l1<=8;l1++)
	for(int r1=1;r1<=8;r1++)
	for(int l2=l1;l2<=8;l2++)
	for(int r2=r1;r2<=8;r2++)
	{
		ll sum=s[l2][r2]-s[l1-1][r2]-s[l2][r1-1]+s[l1-1][r1-1];
		f[l1][r1][l2][r2][1]=sum*sum;
	}
	for(int k=2;k<=n;k++)
	for(int l1=1;l1<=8;l1++)
	for(int r1=1;r1<=8;r1++)
	for(int l2=l1;l2<=8;l2++)
	for(int r2=r1;r2<=8;r2++)
	{
		f[l1][r1][l2][r2][k]=INF;
		for(int t=l1+1;t<=l2;t++)
		{
			ll tmp=min(f[l1][r1][t-1][r2][k-1]+f[t][r1][l2][r2][1],f[t][r1][l2][r2][k-1]+f[l1][r1][t-1][r2][1]);
			f[l1][r1][l2][r2][k]=min(f[l1][r1][l2][r2][k],tmp);
		}
		for(int t=r1+1;t<=r2;t++)
		{
			ll tmp=min(f[l1][r1][l2][t-1][k-1]+f[l1][t][l2][r2][1],f[l1][t][l2][r2][k-1]+f[l1][r1][l2][t-1][1]);
			f[l1][r1][l2][r2][k]=min(f[l1][r1][l2][r2][k],tmp);
		}
	}
	ll ans=f[1][1][8][8][n];
	long double S=(long double)1.0*ans/n - (long double)(1.0*s[8][8]*s[8][8])/n/n;
	printf("%.3Lf",sqrt(S));
	return 0;
}

数位 DP

数位 DP 长期被归到计数类 DP 中,经常以“统计区间 \([L,R]\) 内满足 \(\cdots\) 形式的数” 之类的面孔出现。

常用技巧:

  1. \(f([L,R])\Rightarrow f(R)-f(L-1)\)。让我们求 \([L,R]\) 满足条件的个数,可以转化为求 \([1,L]\) 的个数和求 \([1,R]\) 的个数。
  2. 从数的形式去考虑。

架空干讲是没有用处的。所以我们通过题来感受这种 DP 的特征。

度的数量

求给定区间 \([X,Y]\) 中满足下列条件的整数个数:这个数恰好等于 \(K\) 个互不相等的 \(B\) 的整数次幂之和。

例如,设 \(X=15,Y=20,K=2,B=2\),则有且仅有下列三个数满足题意:

\[\begin{aligned} 17=24+20\\ 18=24+21\\ 20=24+22\\ \end{aligned} \]

输入格式

第一行包含两个整数 \(X\)\(Y\),接下来两行包含整数 \(K\)\(B\)

输出格式

只包含一个整数,表示满足条件的数的个数。

数据范围

\(1≤X≤Y≤2^{31}−1,\\ 1≤K≤20,\\ 2≤B≤10\)

输入样例:

15 20
2
2

输出样例:

3

解析

解释一下题意,我们们要统计在 \([L,R]\) 中,有多少整数在 \(B\) 进制下只有恰好 \(k\)\(1\)

可以通过在 \(B\) 进制下逐位填 \(1\) 来构造一个合法的数。

我们可以设状态 \(f(k)\) 表示区间 \([1,k]\) 中满足条件的整数有多少个。

首先将其转为一个 \(B\) 进制数 \(k_{(B)}=\overline{a_{n-1}a_{n-2}a_{n-3}\cdots a_0}\)

然后我们开始从高到低填数。

对于第 \(i\)\(a_i\),假设我们已经填了 \(last\)\(1\) ,总共需要填 \(k\)\(1\);根据原数当前位 \(a_i\) 的大小情况枚举情况讨论:

假设我们要填在这一位的数是 \(a_i^{\prime}\)

  • \(a_i=0\):这一位只能填 \(0\),没得选,我们要继续看下一个数 \(a_{i-1}\)
  • \(a_i=1\):我们讨论这一位填 \(0\) 还是填 \(1\)
    • \(a_i^{\prime}=0\) 那么后面的位数我们能够随便填,总共有 \(i\) 个位置,有 \(k-last\)\(1\) 要填,方案数就是 \(C_{i}^{k-last}\)
    • \(a_i^{\prime}=1\) 我们就要看下一位怎么填。
  • \(a_i>1\):当前位无论填 \(1\) 还是填 \(0\) 后面的位数都可以随便填,也就是说,总共的方案数有 \(C_{i}^{k-last-1}+C_i^{k-last}\)
  • 边界情况:当我们一直填下去,到 \(a_0\) 时,若 \(last=k\) 也就是刚好填 \(k\)\(1\) 的时候,这也是一种方案。

对于每一位,我们都要考虑是否填最大数字,可以画一个如下的图来理解:

对于每一个节点 \(a_i^{\prime}\) ,我们都有两个分支,左分支一般都可以使用数学公式/预处理直接计算,右分支需要我们逐步分解。

用维恩图表示的y氏Dp分析法,得到这样一个图:

从上面我们可以看出,数位 DP 的状态转移不再是状态和状态之间的转移,而是 DP 状态本身性质方面的转移。或者说,我们划分出来的子问题和原问题不完全相同,是另外一个问题,但是一个问题划分出的子问题都是满足阶段没有后效性,并且有子结构相似的子问题。仍然无法摆脱 DP 的本质。我们在分析问题的时候仍然是抓典型情况进行分析,然后一般化,最后考虑边界情况。

代码可以记搜,也可以像下面这样递推。反正都是逐位枚举的过程。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=40;

ll C[N][N];
void init()//预处理组合数
{
	for(int i=0;i<N;i++)
	{
		for(int j=0;j<=i;j++)
		{
			if(j==0) C[i][j]=1;
			else C[i][j]=C[i-1][j-1]+C[i-1][j];
		}
	}
}

int k,b;

ll dp(ll n)
{
	if(!n) return 0;//特判 0
	vector<int> nums;
	while(n) nums.push_back(n%b),n/=b;
	ll last=0,res=0;

	for(int i=nums.size()-1;i>=0;i--)//从高位向低位逐位遍历
	{
		int x=nums[i];
		if(x)//x>=1 的情况有讨论价值
		{
			res+=C[i][k-last];//最少都有 C(i,last) 的贡献
			if(x>1)
			{
				if(k-last-1>=0) res+=C[i][k-last-1];//加上多出的贡献,注意边界
				break;
			}
			else
			{
				last++;
				if(last>k) break;
			}
		}
		if(!i && last==k) res++;//把最终的方案加上
	}
	return res;
}

int main()
{
	init();
	int x,y;
	scanf("%d%d%d%d",&x,&y,&k,&b);
	int ans=dp(y)-dp(x-1);
	printf("%d",ans);
	return 0;
}

数字游戏

幻想乡里最近很流行数字游戏。

紫命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如 \(123\)\(446\)

现在紫和幽幽子决定玩一个游戏,指定一个整数闭区间 \([a,b]\),问这个区间内有多少个不降数。

输入格式

输入包含多组测试数据。

每组数据占一行,包含两个整数 \(a\)\(b\)

输出格式

每行给出一组测试数据的答案,即 \([a,b]\) 之间有多少不降数。

数据范围

\(1≤a≤b≤2^{31}−1\)

输入样例:

1 9
1 19

输出样例:

9
18

解析

我们参照上一个题,集中精力解决 \([1,L]\) 中有多少个这样的数。

  • 状态设计:设 \(f(n)\) 表示 \([1,n]\) 中满足条件的数有多少个。

接下来我们模拟一下从高位到低位填数的过程。

按照上面的分类方式画出图解:

我们猜测,左分支是可以直接算的,右分支只需要判断 \(n\) 本身是不是满足不下降数的条件即可。

猜归猜,我们还要想想左分支如何去算,先抓一个典型情况来研究。

设第 \(i\) 位填的 \(x < a_i\) ,那就还有 \(i\) 位没有填(数位编号从 \(0\) 开始)。之后的 \(i\) 位可以随便填,只要一个填数方案使得这个数是一个不下降数那就是一个合法方案。

也就是说我们要解决一个“有 \(i\) 个位置,每个位置填 \(0\sim 9\) ,求能填出不下降数的所有方案”的问题。这个问题本身就是一个 DP 问题;这也是数位 DP 的难点,即还需要使用 DP 来预处理。

现在来研究这个问题:

首先仍然是状态设计。

  • \(g(i,j)\)从低到高填了 \(i\) 位,最高位是 \(j\) 的不下降数所有方案数总和。

寻找最后一个不同点。由于最高位已经确定了,最后一个不同点就是倒数第二位,设为 \(k\)

枚举 \(k\) 的所有情况,我们能将集合划分如下:

\(g\) 是一个带有总和性质的状态,应该囊括子集合中的所有状态,由此可以得到转移方程如下:

  • \[g(i,j)=\sum_{k=j}^9g(i-1,k) \]

继续考虑边界情况得到:特别的,\(g(1,j)=1\)

这就是左分支的计算方法,\(g\) 直接预处理即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll g[35][10];

void init()
{
	for(int i=0;i<=9;i++) g[1][i]=1;
	for(int i=2;i<=20;i++)
	{
		for(int j=0;j<=9;j++)
		{
			for(int k=j;k<=9;k++)
				g[i][j]+=g[i-1][k];
		}
	}
}

ll dp(ll n)
{
	if(!n) return 1;
	vector<int> nums;
	while(n) nums.push_back(n%10),n/=10;

	ll res=0,last=0;//答案,上一位上什么数
	for(int i=nums.size()-1;i>=0;i--)
	{
		int x=nums[i];
		for(int j=last;j<x;j++)//左分支
			res+=g[i+1][j];
		if(x<last) break;//没有办法往下填
		last=x;
		if(!i) res++;//右分支走到底
	}
	return res;
}

int main()
{
	init();
	int l,r;
	while(scanf("%d%d",&l,&r)!=EOF)
		printf("%lld\n",dp(r)-dp(l-1));
	return 0;
}


Windy 数

Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为 \(2\) 的正整数被称为 Windy 数。

Windy 想知道,在 \(A\)\(B\) 之间,包括 \(A\)\(B\),总共有多少个 Windy 数?

输入格式

共一行,包含两个整数 \(A\)\(B\)

输出格式

输出一个整数,表示答案。

数据范围

\(1≤A≤B≤2×10^9\)

输入样例1:

1 10

输出样例1:

9

输入样例2:

25 50

输出样例2:

20

解析

\([L,R]\)不含前导 \(0\) 的情况下任意相邻两位差 \(\ge 2\) 的数的个数。

这题与之前的两道题最大的不同就是多了一个叫前导 \(0\) 的东西。

前导 \(0\) 在这个题中显然会对 Windy 数的判定产生影响,比如: \(135\)\(0135\) 。其中前者符合形式规定而后者不符合。所以我们要先解决前导 \(0\) 的问题。

但是在这之前,先形式化地定义状态:

  • \(f(n)\)\([1,n]\) 中 Windy 数的个数。

观察这个状态:我们输入的是一个无前导 \(0\) 整数 \(n\),但是现在枚举过程中有了前导 \(0\)。这说明什么?这说明这个数的位数比 \(n\) 少,换言之,这个数一定小于 \(n\)

我们之前的两道数位 DP 都是将小于原数的方案直接计算出来或预处理得到其方案数的,前导 \(0\) 只是一种子情况当然可以用处理左分支的方法处理出来。

现在我们处理了所有前导 \(0\) 的情况,只需要考虑没有前导 \(0\) 的情况。

仍然先画出填数顺序的树形图:

注意,与上两道题不同的是:我们考虑的是没有前缀零的情况,所以最高位要从 \(1\) 开始枚举。

右分支走到底就是原数,我们只要判定原数是不是 Windy 数即可。关键在于左分支怎么计算。

仍然抓典型来分析:

\(n=\overline{a_{k-1}a_{k-2}a_{k-3}\dots\ a_0}\)

假设第 \(i\) 位填的数是 \(x<a_i\) ,我们已经只剩 \(i\) 位数没填,由于这个数已经小于原数,这 \(i\) 位数我们可以随便填,只要满足 Windy 数的性质。

  • \(g(i,j)\) 表示从低到高填了 \(i\) 位,最高位填 \(j\) 的所有构造方案中能构造出 Windy 数的方案总数。

寻找这些构造方法的最后一个不同点,也就是倒数第二位的取值,然后以它为分类标准来划分状态表示的集合:

注意,与 \(j\) 相差不到 \(2\) 的不能合法转移到状态 \(g(i,j)\)

\(g(i,j)\) 是一个基于数量总和属性的状态值,应当是其表示集合中所有划分出来的子集的状态值的和。

用转移式说话:

  • \[g(i,j)=\sum\limits_{k\in[0,9],|j-k|\ge 2}g(i-1,k) \]

继续思考边界,得到特殊情况:特别的,\(g(1,j)=0\)

我们发现,这样定义的状态,不仅解决了左分支问题,也解决了前导 \(0\) 的问题。

code:

#include <bits/stdc++.h>
using namespace std;

int g[35][10];

void init()
{
	for(int i=0;i<=9;i++) g[1][i]=1;
	for(int i=2;i<=25;i++)
	{
		for(int j=0;j<=9;j++)
			for(int k=0;k<=9;k++)
				if(abs(j-k)>=2) g[i][j]+=g[i-1][k];
	}
}

int dp(int n)
{
	if(!n) return 0;

	vector<int> nums;
	while(n) nums.push_back(n%10),n/=10;
	int res=0,last=-2;// last 的初值设为和 0~9 之间每个数地方的差都大于等于 2 的值

    /*不带前导 0 的数*/
	for(int i=nums.size()-1;i>=0;i--)
	{
		int x=nums[i];
		for(int j=(i==nums.size()-1);j<x;j++)//注意特判最高位
			if(abs(j-last)>=2)	res+=g[i+1][j];

		if(abs(x-last)>=2) last=x;//满足要求继续往下
		else break;//右分支结束,退出

		if(!i) res++;//右分支完全合法
	}

	/*带前导 0 的数*/
	for(int i=1;i<nums.size();i++)//枚举带前导 0 的数的有效位数
		for(int j=1;j<=9;j++)//枚举这个数的最高位
			res+=g[i][j];

	return res;
}

int main()
{
	int A,B;
	init();
	scanf("%d%d",&A,&B);
	return 0&printf("%d",dp(B)-dp(A-1));
}

打了三道题,我们能够发现它们的结构形式都非常统一。

怪不得大家都说数位 DP 只需要背板子。


数字游戏-Pro

幻想乡最近真的很流行数字游戏。

紫又命名了一种取模数,这种数字必须满足各位数字之和 \(\bmod N\)\(0\)

现在紫和幽幽子又要玩游戏了,指定一个整数闭区间 \([a.b]\),问这个区间内有多少个取模数。

输入格式

输入包含多组测试数据,每组数据占一行。

每组数据包含三个整数 \(a,b,N\)

输出格式

对于每个测试数据输出一行结果,表示区间内各位数字和 \(\bmod N\)\(0\) 的数的个数。

数据范围

\(1≤a,b≤2^{31}−1,\\ 1≤N<100\)

输入样例:

1 19 9

输出样例:

2

解析

还是区间里面的数满足性质:各位数之和等于给定数的整倍数。

但是,这里的性质是整体的性质,更加复杂了。

不要怕,我们一步步来分析。

首先仍然先画出树形图:

对于右分支,我们仍然只需判定原数是否合法。

考虑左分支怎么做。

抓典型情况来分析。

假设我们已经按照原数填了 \(i\) 位,第 \(i-1\) 位填了一个 \(x< a_{i-1}\) 那么就有 \(i-1\) 位还没有填,这 \(i-1\) 位只要能让填出的数各数位之和为 \(\bmod N=0\) 就是合法的。

也就是说,我们的目标是填 \(i-1\) 个数 \(\{b_{i-1}\}\) 使得:

\[\sum_{y=i}^{n-1}a_y+x+\sum_{z=1}^{i-1}b_z\equiv 0 \mod N \]

由于 \(a_y\) 项和 \(x\) 项已知,我们试着移项,得到:

\[\sum_{z=1}^{i-1}b_z\equiv -(\sum_{y=i}^{n-1}a_y+x)\mod N \]

也就是说,这 \(i-1\) 位的填法要满足上面的条件。

我们试着设计状态:

  • \(f(i,j,k)\) 为从低到高填了 \(i\) 位数,最高位为 \(j\) ,各位数之和 \(\bmod N=k\) 的所有方案数。

寻找最后一个不同点,按照倒数第二位的填法归类,得到:

将划分出来的子集用一个通式表示出来,就是 \(f(i-1,x,(k-j)\bmod N)\)

根据状态值的性质,我们可以得到状态转移方程:

  • \[f(i,j,k)=\sum_{x=0}^9 f(i-1,x,(k-j)\bmod N) \]

边界情况: \(f(1,j,j\bmod N)=1\)

至此左分支和右分支的问题都解决了。

code

#include <bits/stdc++.h>
using namespace std;

int a,b,N;
int f[100][12][123];

void init()
{
	memset(f,0,sizeof f);

	for(int i=0;i<=9;i++) f[1][i][i%N]=1;
	for(int i=2;i<=11;i++)
	{
		for(int j=0;j<=9;j++)
			for(int k=0;k<N;k++)
				for(int x=0;x<=9;x++)
					f[i][j][k]+=f[i-1][x][((k-j)%N+N)%N];
	}
}


int dp(int n)
{
	if(!n) return 1;
	vector<int> nums;
	while(n) nums.push_back(n%10),n/=10;

	int res=0,last=0;
	for(int i=nums.size()-1;i>=0;i--)
	{
		int x=nums[i];
		for(int j=0;j<x;j++)
			res+=f[i+1][j][((-last)%N+N)%N];
		last+=x;
		if(!i) if(last%N==0) res++;
	}
	return res;
}

int main()
{
	ios::sync_with_stdio(0);
	while(cin>>a>>b>>N)
	{
		init();
		cout<<(dp(b)-dp(a-1))<<"\n";
	}
	return 0;

}


不要 62

杭州人称那些傻乎乎粘嗒嗒的人为 \(62\)(音:laoer)。

杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。

不吉利的数字为所有含有 \(4\)\(62\) 的号码。例如:\(62315,73418,88914\) 都属于不吉利号码。但是,\(61152\) 虽然含有 \(6\)\(2\),但不是 \(6,2\) 连号,所以不属于不吉利数字之列。

你的任务是,对于每次给出的一个牌照号区间 \([n,m]\),推断出交管局今后又要实际上给多少辆新的士车上牌照了。

输入格式

输入包含多组测试数据,每组数据占一行。

每组数据包含一个整数对 \(n\)\(m\)

当输入一行为 0 0 时,表示输入结束。

输出格式

对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置。

数据范围

\(1≤n≤m≤10^9\)

输入样例:

1 100
0 0

输出样例:

80

解析

限制开始乱七八糟,但是我们别乱了阵脚 ~~skr.~

还是先解决 \([1,L]\) 的问题然后利用前缀和思想。

首先还是化树形图。

右分支仍然是特判原数是否符合规则。

考虑左分支做法。

抓典型分析:

我们假设在第 \(i\) 位填了一个数 \(x<a_i\),那么还有 \(i\) 位没填,这 \(i\) 位是可以随便填的,只要满足题中合法数的性质。也就是说,不能有 \(4\) 和相邻的 \(62\)。如果我们从低到高一位一位填的话,填了 \(2\) 之后就不能填 \(6\),而无论何时都不能填 \(4\),其他时候随意。

我们可以把这个过程用状态机来表示出来。时间轴就是我们从低到高填数的顺序,状态就是每一位上的数字。状态之间的转移体现的就是上一个数与这一个数的过程。用拓补图画出来就是这样:

除了虚线和自环边以外,其他都是双向边。

照着状态机写 DP。

  • \(f(i,j)\) 为从低到高填了 \(i\) 位,最高位是 \(j\) 的所有合法方案总数。

根据上面的状态机模型,得到状态转移方程:

\[f(i,j)=[j\ne 4]\sum_{k=0}^9[\lnot(j=6\land k=2 )\land k\ne 4]f(i-1,k) \]

思考边界情况:\(f(1,j)=[j\ne 4]\)

左分支的预处理就是这样。

code:

#include <bits/stdc++.h>
using namespace std;

int f[20][10];

void init()
{
	for(int i=0;i<=9;i++) f[1][i]=int(i!=4);
	for(int i=2;i<=11;i++)
	{
		for(int j=0;j<=9;j++)
			if(j!=4)
			{
				for(int k=0;k<=9;k++)
				{
					if(!(j==6&&k==2)&&k!=4)
					f[i][j]+=f[i-1][k];
				}
			}
	}
}

int dp(int n)
{
	if(!n) return 1;

	vector<int> nums;
	while(n) nums.push_back(n%10),n/=10;

	int res=0,last=0;
	for(int i=nums.size()-1;i>=0;i--)
	{
		int x=nums[i];
		for(int j=0;j<x;j++)
			if(j!=4&&!(last==6&&j==2)) res+=f[i+1][j];
		if((last==6&&x==2)||x==4) break;
		last=x;
		if(!i) res++;
	}
	return res;
}

int main()
{
	init();
	int l,r;
	while(scanf("%d%d",&l,&r)!=EOF&&l&&r)
		printf("%d\n",dp(r)-dp(l-1));
	return 0;
}

恨7不成妻

DS 级码农吉哥依然单身!!!

所以,他平生最恨情人节,不管是 \(214\) 还是 \(77\),他都讨厌!

吉哥观察了 \(214\)\(77\) 这两个数,发现:

\[2+1+4=7\\ 7+7=7×2\\ 77=7×11\\ \]

最终,他发现原来这一切归根到底都是因为和 \(7\) 有关!

所以,他现在甚至讨厌一切和 \(7\) 有关的数!

什么样的数和 \(7\) 有关呢?

如果一个整数符合下面三个条件之一,那么我们就说这个整数和 \(7\) 有关:

  • 整数中某一位是 \(7\)
  • 整数的每一位加起来的和是 \(7\) 的整数倍;
  • 这个整数是 \(7\) 的整数倍。

现在问题来了:吉哥想知道在一定区间内和 \(7\) 无关的整数的平方和。

输入格式

第一行包含整数 \(T\),表示共有 \(T\) 组测试数据。

每组数据占一行,包含两个整数 \(L\)\(R\)

输出格式

对于每组数据,请计算 \([L,R]\) 中和 \(7\) 无关的数字的平方和,并将结果对 \(10^9+7\) 取模后输出。

数据范围

\(1≤T≤50, 1≤L≤R≤10^{18}\)

输入样例:

3
1 9
10 11
17 17

输出样例:

236
221
0

解析

求平方和?还有一堆莫名其妙的性质?

这题也有够 ex 的。

三个性质还好说,这求平方和可比求存在个数难不知道几个阶级。

还是先一步一步分析吧。

我们先画出树形图。由于前导 \(0\) 不会影响三个性质中任何一条的判定,我们可以不用单独考虑。

对于右分支我们还是套路地特判,主要看左分支。

抓典型情况来分析:

条件涉及到这个数本身的大小 \(\bmod 7\) ,这个数的数位之和 \(\bmod 7\) ,所以我们可以如下设计状态:

  • \(f(i,j,a,b)\) 为从低到高填了 \(i\) 位,最高位是 \(j\),各位数之和 \(\bmod 7\)\(a\) ,组成的数字 \(\bmod 7\)\(b\) 的数的平方和。

寻找最后一个不同点,也就是倒数第二位的取值,按照它来划分状态集合:

我们枚举倒数第二位的取值 \(k\) ,那么当前状态转移自 \(f(i-1,k,(a-j)\bmod 7,(b-j\times 10^{i-1})\bmod 7)\)

但是,平方和不能单纯的相加,我们要另想办法。

将当前状态值的组成写出来:

\[(\overline{jA_1})^2+(\overline{jA_2})^2+\cdots+(\overline{jA_t})^2 \]

其中 \(A\) 表示后面 \(i-1\) 位填的数,\(t\) 代表有多少个合乎要求的数。

把它拆开:

\[\begin{aligned} &\quad(j\times 10^{i-1}+A_1)^2+(j\times 10^{i-1}+A_2)^2+(j\times 10^{i-1}+A_3)+\cdots +(j\times 10^{i-1}+A_t)\\ &=t(j\times 10^{i-1})^2+2j\times 10^{i-1}(A_1+A_2+A_3+\cdots+A_t)+(A_1^2+A_2^2+A_3^2+\cdots +A_t^2)\\ \end{aligned} \]

观察式子,我们发现有几个关键值需要我们求:\(t,\sum\limits_{i=1}^tA_i,\sum\limits_{i=1}^tA_i^2\)

\(t\) 是合法数的个数,我们仍然可以设 \(h(i,j,a,b)\) 来通过类似的 DP 过程求出, \(h(i,j,a,b)=\sum_{k=0}^9h(i-1,k,(a-j)\bmod 7,(b-j\times 10^{i-1})\bmod 7)\)

同理我们可以设 \(g(i,j,a,b)\) 通过类似的方式划分集合,推一推式子可以得到 \(g(i,j,a,b)=\sum_{k=0}^9(h(i-1,k,(a-j)\bmod 7,(b-j\times 10^{i-1})\bmod 7)\times j\times 10^{i-1}+g(i-1,k,(a-j)\bmod 7,(b-j\times 10^{i-1})\bmod 7)\)

预处理一下就好了。

现在我们要思考如何统计答案。

此时,前面已经 \(n-1\sim i+1\) 位已经填好了数,枚举第 \(i\) 位填 \(x\) 使得后面的位数都 \(0\sim 9\) 可以随便填。前面各位数字之和是 \(last_1=a_{n-1}+a_{n-2}+a_{n-3}+\dots+ a_{i+1}\),前面填的数字组成的数字 \(last_2=\overline{a_{n-1}a_{n-2}a_{n-3}\dots a_{i+1}}\)

我们假设后面的各位数字的和为 \(A\),后面的各位数字组成的数为 \(B\),那么题目要求:

  1. 不含有 \(7\)
  2. \(A+last_1\not\equiv 0 \mod 7\)
  3. \(B+last_2\times10^{i+1}\not\equiv0 \mod 7\)

移项,得到 \(\begin{cases}A\not\equiv -last_1\mod 7,\\ B\not\equiv-last_2\times 10^{i+1}\mod 7\end{cases}\)

也就是说,我们再做一遍上面求平方和公式,按照上面的条件将不合法的状态筛去即可。

code:(仅保证思路正确,不保证取模等细节的正确性)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=1e5+10;
const ll MOD=1e9+7;

ll mod(ll x,ll p) {return (x%p+p)%p;}

ll f[35][10][10][10];//平方和
ll g[35][10][10][10];//和
ll h[35][10][10][10];//个数
ll pow7[30],powm[30];

void init()
{
    for(int i=0;i<=9;i++)
    {
        if(i==7) continue;
        f[1][i][mod(i,7)][mod(i,7)]+=i*i;
        g[1][i][mod(i,7)][mod(i,7)]+=i;
        h[1][i][mod(i,7)][mod(i,7)]++;
    }
    ll t=10;
    for(int i=2;i<=19;i++)
    {
        t=t*10;
        for(int j=0;j<=9;j++)
        {
            if(j==7) continue;
            for(int a=0;a<7;a++)
            {
                for(int b=0;b<7;b++)
                {
                    for(int k=0;k<=9;k++)
                    {
                        if(k==7) continue;
                        h[i][j][a][b]=mod(h[i][j][a][b]+h[i-1][k][mod(a-j,7)][mod(b-j*t,7)],MOD);

                        g[i][j][a][b]=mod(g[i][j][a][b]+mod(mod(h[i-1][k][mod(a-j,7)][mod(b-j*t,7)]*j*mod(t,MOD),MOD)+g[i-1][k][mod(a-j,7)][mod(b-j*t,7)],MOD),MOD);

                        f[i][j][a][b]=mod(f[i][j][a][b]
                                + mod(h[i-1][k][mod(a-j,7)][mod(b-j*t,7)]*mod(j*j*mod(mod(t,MOD)*mod(t,MOD),MOD),MOD),MOD)
                                + mod(mod(2*j*mod(t,MOD),MOD)*g[i-1][k][mod(a-j,7)][mod(b-j*t,7)],MOD)
                                + mod(f[i-1][k][mod(a-j,7)][mod(b-j*t,7)],MOD),MOD);
                    }
                }
            }
        }
    }
    pow7[0]=powm[0]=1;
    for(int i=1;i<=20;i++)
    {
        pow7[i]=mod(pow7[i-1]*10,7);
        powm[i]=mod(powm[i-1]*10,MOD);
    }
}

ll dp(ll n)
{
    int nn=mod(n,MOD);
    if(n==0) return 0;
    vector<ll> nums;
    while(n>0) nums.push_back(n%10),n/=10;

    ll res=0,last=0,last2=0;
    for(int i=nums.size()-1;i>=0;i--)
    {
        ll x=nums[i];
        for(int j=0;j<x;j++)
        {
            if(j==7) continue;
            ll aa=mod(-last,7);
            ll bb=mod(-last2*pow7[i+1],7);
            ll ff=0,gg=0,hh=0;//平方和,和,个数

			/*计算对应的值*/
            for(int a=0;a<7;a++)
                for(int b=0;b<7;b++)
                {
                    if(a==aa||b==bb) continue;
                    ff=mod(ff+f[i+1][j][a][b],MOD);
                    gg=mod(gg+g[i+1][j][a][b],MOD);
                    hh=mod(hh+h[i+1][j][a][b],MOD);
                }

            res=mod(res
             + mod(mod(mod((last2%MOD)*(last2%MOD),MOD)*mod(mod(powm[i+1],MOD)*mod(powm[i+1],MOD),MOD),MOD)*hh,MOD)
             + mod(mod(2*last2,MOD)*mod(powm[i+1]*gg,MOD),MOD)
             + mod(ff,MOD),MOD);//再做一遍公式
        }
        if(x==7) break;
        last+=x; last2=last2*10LL+x;

        if(!i&&mod(last,7)!=0&&mod(nn,7)!=0) res=mod(res+mod(mod(nn,MOD)*mod(nn,MOD),MOD),MOD);
    }
    return res;
}

int main()
{
    init();
    int T;
    scanf("%d",&T);
    while(T--)
    {
        ll l,r;
        scanf("%lld%lld",&l,&r);
        printf("%lld\n",dp(r)-dp(l-1));
    }
    return 0;
}
posted @ 2021-04-28 16:26  RemilaScarlet  阅读(310)  评论(0编辑  收藏  举报