算法模板(4):动态规划(3) 做题积累(1)

news2024/11/18 9:43:00

动态规划

1. 背包

1. 1024. 装箱问题

  • 题意:有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积(正整数)。要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
  • 别学那么死板。换种说法,每个物品体积是v,价值是v,此时的f[V]表示的就是体积不超过V的情况下,体积的最大值。然后答案就是 V − f [ V ] V - f[V] Vf[V]
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 35, maxv = 20010;
int f[maxv], v[maxn], V, N;
int main() {
	scanf("%d%d", &V, &N);
	for (int i = 1; i <= N; i++) {
		scanf("%d", &v[i]);
	}
	for (int i = 1; i <= N; i++) {
		for (int j = V; j >= v[i]; j--) {
			f[j] = max(f[j], f[j - v[i]] + v[i]);
		}
	}
	printf("%d\n", V - f[V]);
	return 0;
}

2. 734. 能量石

  • 题意:吃完第 i i i 块能量石需要花费的时间为 S i S_i Si秒。第 i i i 块能量石最初包含 E i E_i Ei单位的能量,并且每秒将失去 L i L_i Li单位的能量。能量石中包含的能量最多降低至 0 0 0。请问杜达通过吃能量石可以获得的最大能量是多少?
  • 贪心思路:吃第 i i i 块儿石头和第 i + 1 i + 1 i+1 块儿石头顺序的区别: E i ′ + E i + 1 ′ − S i ∗ L i + 1 E_i' + E_{i+1}'-S_i*L_{i+1} Ei+Ei+1SiLi+1 E i ′ + E i + 1 ′ − S i + 1 ∗ L i E_i' + E_{i+1}'-S_{i+1}*L_{i} Ei+Ei+1Si+1Li. 则 S i E i \frac{S_i}{E_i} EiSi越小,获得的能量越多。不过在结构体里面重载函数时,可以写成 S i ∗ L i + 1 < L i + 1 ∗ S i S_i*L_{i+1}<L_{i+1}*S_i SiLi+1<Li+1Si.
  • 按照上述排完序之后,然后设 f ( i , j ) f(i, j) f(i,j)为从前 i i i 个物品中选,且总体积恰好 j j j 的方案的能量最大值。则 f ( i , j ) = max ⁡ { f ( i − 1 , j ) , f ( i − 1 , j − s ) + e − ( j − s ) ∗ l } f(i, j) = \max\{f(i - 1, j), f(i - 1, j - s) + e - (j - s) * l\} f(i,j)=max{f(i1,j),f(i1,js)+e(js)l}. 不过不用管能量是否减到负数。因为这种情况会被不选这种物品的方案替代掉.
  • zzh认为:正常的背包问题,物品价值不随时间改变,也就是说选择的顺序不影响答案。但是这个题选择的顺序会影响答案,因此找到一组可行的选择组合时,要按照贪心的性质进行选择,保证顺序是最优的. 当然这个题还有一个性质,就是能量石的能量不会降到负数,因此需要dp. 如果可以是负数的话,那么排完序之后按照顺序从前到后选择即可.
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxv = 10010, maxn = 110, V = 10000;
int f[maxv], N, kase;
struct P{
	int e, s, l;
	bool operator<(const P& rhp)const {
		return s * rhp.l < l* rhp.s;
	}
}G[maxn];
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		scanf("%d", &N);
		memset(f, -0x3f, sizeof f);
		f[0] = 0;
		for (int i = 0; i < N; i++) {
			scanf("%d%d%d", &G[i].s, &G[i].e, &G[i].l);
		}
		sort(G, G + N);
		for (int i = 0; i < N; i++) {
			int s = G[i].s, e = G[i].e, l = G[i].l;
			for (int j = V; j >= s; j--) {
				f[j] = max(f[j], f[j - s] + e - (j - s) * l);
			}
		}
		int ans = 0;
		for (int i = 0; i <= V; i++) ans = max(f[i], ans);
		printf("Case #%d: %d\n", ++kase, ans);
	}
	return 0;
}

3. Coins

  • 题意:给定 n n n 种硬币,其中第 i i i 种硬币的面值为 A i A_i Ai,共有 C i C_i Ci 个. 问 1 ∼ M 1 \sim M 1M 之间能被凑成的面值有多少个? 1 ≤ n ≤ 100 , 1 ≤ m ≤ 1 0 5 , 1 ≤ A i ≤ 1 0 5 , 1 ≤ C i ≤ 1000 1 \le n \le 100,1 \le m \le 10^5,1\le A_i \le 10^5,1 \le C_i \le 1000 1n100,1m105,1Ai105,1Ci1000.

  • u s d [ j ] usd[j] usd[j] 表示 f [ j ] f[j] f[j] 在选择第 i i i 种硬币的时候,需要的最少硬币数。那么在 f [ j − a [ i ] ] f[j - a[i]] f[ja[i]] 已经为 t r u e true true 的时候,如果 f [ j ] f[j] f[j] t r u e true true,就不进行状态转移,并令 u s e d [ j ] = 0 used[j] = 0 used[j]=0. 如果 f [ j ] f[j] f[j] f a l s e false false,就进行 u s e d [ j ] = u s e d [ j − a [ i ] ] + 1 used[j] = used[j - a[i]] + 1 used[j]=used[ja[i]]+1 的转移.

#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = 100010;
bitset<M> f;
int used[M], a[N], c[N];
int n, m;
int main()
{
    while(cin >> n >> m, n)
    {
        f.reset();
        f[0] = 1;
        for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
        for(int i = 1; i <= n; i++) scanf("%d", &c[i]);
        for(int i = 1; i <= n; i++)
        {
            fill(used + 1, used + m + 1, 0);
            for(int j = a[i]; j <= m; j++)
            {
                if(!f[j] && f[j - a[i]] && used[j - a[i]] < c[i])
                {
                    f[j] = 1, used[j] = used[j - a[i]] + 1;
                }
            }
        }
        //把0扣掉
        int ans = f.count() - 1;
        printf("%d\n", ans);
    }
}

4. 1013. 机器分配

  • 题意:把 M M M 台机器分配给 N N N 个公司,给一个 N ∗ M N*M NM 的矩阵, w ( i , j ) w(i,j) w(i,j)表示第 i i i 个公司分配 j j j 台机器的盈利。求最大收益与方案。
  • 机器数量是背包容量,每个公司是一组物品,每组物品只能选一个。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 20;
int w[maxn][maxn], f[maxn][maxn], N, M, ans[maxn];
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= M; j++) {
			scanf("%d", &w[i][j]);
		}
	}
	for (int i = 1; i <= N; i++) {
		for (int j = 0; j <= M; j++) {
			f[i][j] = f[i - 1][j];
			for (int k = 1; k <= j; k++) {
				f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
			}
		}
	}
	int j = M;
	for (int i = N; i >= 1; i--) {
		for (int k = 0; k <= M; k++) {
			if (j >= k && f[i][j] == f[i - 1][j - k] + w[i][k]) {
				ans[i] = k;
				j -= k;
				break;
			}
		}
	}
	printf("%d\n", f[N][M]);
	for (int i = 1; i <= N; i++) {
		printf("%d %d\n", i, ans[i]);
	}
	return 0;
}

5. 487. 金明的预算方案

  • 题意:每个主件可以有0个、1个或2个附件(如果想买附件必须先买主件)。他希望在不超过V元的前提下,使每件物品的价格与重要度的乘积的总和最大。
  • 思路不难,代码确实不是很好设计。看看y总怎么用二进制枚举的。
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
const int maxv = 32010, maxn = 70;
typedef pair<int, int> P;
P master[maxn];
vector<P> servent[maxn];
int f[maxv], N, V;
int main() {
	scanf("%d%d", &V, &N);
	for (int i = 1; i <= N; i++) {
		int v, p, q;
		scanf("%d%d%d", &v, &p, &q);
		if (!q) master[i] = { v, v * p };
		else servent[q].push_back({ v, v * p });
	}

	for (int i = 1; i <= N; i++) {
		if (!master[i].first) continue;
		for (int j = V; j >= 0; j--) {
			auto& sv = servent[i];
			for (int k = 0; k < 1 << sv.size(); k++) {
				int v = master[i].first, w = master[i].second;
				for (int u = 0; u < sv.size(); u++) {
					if (k >> u & 1) {
						v += sv[u].first;
						w += sv[u].second;
					}
				}
				if (j >= v) f[j] = max(f[j], f[j - v] + w);
			}
		}
	}
	printf("%d\n", f[V]);
	return 0;
}

2. 组合计数

1. 532. 货币系统

  • 在网友的国度中共有  n n n 种不同面额的货币,第  i i i 种货币的面额为  a [ i ] a[i] a[i],你可以假设每一种货币都有无穷多张。为了方便,我们把货币种数为  n n n、面额数组为  a [ 1.. n ] a[1..n] a[1..n] 的货币系统记作  ( n , a ) (n,a) (n,a)。 找到一个 ( m , b ) (m, b) (m,b) ( n , a ) (n, a) (n,a) 等价,且 m m m 最小。那么有如下性质:
  1. { b } \{b\} {b} 一定可以把 { a } \{a\} {a} 中的每一个数都表示出来。
  2. b i b_i bi一定是从 { a } \{a\} {a}中选出来的。
  3. b i b_i bi一定不能从 { b } \{b\} {b}中的其他数字表示出来。
    用方案数就可以判断出来, f ( a i ) = 1 f(a_i)=1 f(ai)=1时,就是不能被其他 a i a_i ai 表示出来的,那就必须要选择了。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxv = 25010, maxn = 110;
int a[maxn], f[maxv], V, N;
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		V = 0;
		memset(f, 0, sizeof f);
		f[0] = 1;
		scanf("%d", &N);
		for (int i = 1; i <= N; i++) {
			scanf("%d", &a[i]);
			V = max(V, a[i]);			
		}
		for (int i = 1; i <= N; i++) {
			for (int j = a[i]; j <= V; j++) {
				f[j] += f[j - a[i]];
			}
		}
		int ans = 0;
		for (int i = 1; i <= N; i++) {
			ans += (f[a[i]] == 1);
		}
		printf("%d\n", ans);
	}
	return 0;
}

2. C. On the Bench

设集合 A 有 n 1 n_1 n1 a 1 a_1 a1, n 2 n_2 n2 a 2 a_2 a2,…, n m n_m nm a m a_m am 问使其相邻两个数不相同的排列有多少种.

  • 多重集合交错排列

  • 如果把这 n 个数分块,块的内部都是同一组的数字,并且分块儿的过程忽视掉快与块之间的顺序,相当于集合的概念。那么,按照容斥原理,答案应该是:n块的排列,减去n-1块的排列(有连续的两个数字是同一组,而在算dp的过程中,dp[n-1] 已经包含了这个情况的所有方案)……然后就是这样的容斥原理。 a n s = d p [ n ] n ! − d p [ n − 1 ] ( n − 1 ) ! + ⋯ + ( − 1 ) ( n − i ) d p [ i ] i ! + . . . − d p [ 1 ] ∗ 1 ! + d p [ 0 ] ∗ 0 ! ans=dp[n]n!−dp[n−1](n−1)!+⋯+(−1)^{(n−i)}dp[i]i!+...-dp[1]*1! + dp[0]*0! ans=dp[n]n!dp[n1](n1)!++(1)(ni)dp[i]i!+...dp[1]1!+dp[0]0!

  • d p [ i ] [ k ] dp[i][k] dp[i][k] 表示考虑前 i 种数分成 k 块的个数,那么,
    d p [ i ] [ k ] = ∑ j = 1 k d p [ i − 1 ] [ k − j ] C n i − 1 j − 1 ∗ n i ! j ! dp[i][k]=∑_{j=1}^kdp[i−1][k−j]C_{n_i−1}^{j−1}∗\frac{n_i!}{j!} dp[i][k]=j=1kdp[i1][kj]Cni1j1j!ni!

    • d p [ 0 ] [ 0 ] = 1. dp[0][0] = 1. dp[0][0]=1.
    • 因为不考虑块与块之间的顺序,所以可以看作前 k − j k-j kj 个块放在前面,然后把后面的第 i i i 组数字分成 j j j 组,就是隔板法+排列 n i ! C n i − 1 j − 1 n_i!C_{n_i-1}^{j-1} ni!Cni1j1,但是这 j j j 组是无序的,所以再除以 j ! j! j!
for (auto p : tot) {
    cnt.push_back(p.second);
}
int sz = tot.size();

dp[0][0] = 1;
for (int i = 1; i <= sz; i++) {
    for (int k = 1; k <= N; k++) {
        for (int j = 1; j <= min((int)cnt[i - 1], k); j++) {
            dp[i][k] = (dp[i][k] + dp[i - 1][k - j] * C(cnt[i - 1] - 1, j - 1LL) % mod
                        * fact[cnt[i - 1]] % mod * infact[j] % mod) % mod;
        }
    }
}
ll ans = 0;
for (ll i = N; i >= 0; i--) {
    ll f = ((N - i) & 1) ? -1LL : 1LL;
    ans +=  f * dp[sz][i] % mod * fact[i] % mod;
    ans = (ans % mod + mod) % mod;
}
printf("%lld\n", ans);

3. 线性DP

1. 1018. 最低通行费

  • ( 1 , 1 ) (1,1) (1,1) ( n , n ) (n, n) (n,n) 找到一个长度为 2 n − 1 2n - 1 2n1 的路径使得路径点权之和最小。
  • 这道题也是一个 N ∗ N N*N NN 的方格。不过行走方向没有限制,但是限制走的步数为 2 ∗ N − 1 2*N-1 2N1 步。但是,从 ( 1 , 1 ) (1, 1) (1,1) ( N , N ) (N, N) (N,N) 的曼哈顿距离就是 2 ∗ N − 1 2*N-1 2N1 啊,所以这道题最少要走​ 2 ∗ N − 1 2*N-1 2N1 步。
  • 不过,这个题求的使最小值,尤其小心边界问题。最小值的话,还是要把 f f f 初始化为 I N F INF INF 的。 不然就难以保证从 ( 1 , 1 ) (1, 1) (1,1) 进入迷宫。
const int maxn = 110;
int f[maxn][maxn], N;
int main() {   
	memset(f, 0x3f, sizeof f);
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) {
			scanf("%d", &f[i][j]);
		}
	}
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) {
			if (i == 1 && j == 1) continue;
			f[i][j] = min(f[i - 1][j] + f[i][j], f[i][j - 1] + f[i][j]);
		}
	}
	printf("%d\n", f[N][N]);
	return 0;
}

2. 482. 合唱队形

  • 这道题是找一个这样的最长子序列: T 1 < T 2 < . . . < T i > T i + 1 > T i + 2 > . . . > T k T_1 < T_2 < ... < T_i > T_{i+1} > T_{i+2} > ...>T_{k} T1<T2<...<Ti>Ti+1>Ti+2>...>Tk。这个正着求一次最长上升子序列,再倒着求一次最长下降子序列。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 110;
int a[maxn], f1[maxn], f2[maxn];
int N;
int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
	for (int i = 1; i <= N; i++) {
		f1[i] = 1;
		for (int j = 1; j <= i; j++) {
			if (a[j] < a[i]) f1[i] = max(f1[i], f1[j] + 1);
		}
	}
	for (int i = N; i > 0; i--) {
		f2[i] = 1;
		for (int j = N; j >= i; j--) {
			if (a[i] > a[j]) f2[i] = max(f2[i], f2[j] + 1);
		}
	}
	int res = 0;
	for (int i = 1; i <= N; i++) {
		res = max(res, f1[i] + f2[i] - 1);
	}
	printf("%d\n", N - res);
	return 0;
}

3. 编辑距离

题意:给定 n n n 个长度不超过 10 10 10 的字符串以及 m m m 次询问,每次询问给出长度不超过 10 10 10 的一个字符串和一个操作次数上限。对于每次询问,请你求出给定的 n n n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。每个对字符串进行的单个字符的插入、删除或替换算作一次操作。 1 ≤ n , m ≤ 1000 1\le n,m \le 1000 1n,m1000.

注意这道题,遇到字符串容易出错。我们这道题向函数传入字符串的时候不可以是 s + 1,因为在计算递推式的时候,下标是从1开始的,如果传入s + 1,那么字符串下标其实是从2开始的。

#include<bits/stdc++.h>
using namespace std;
const int N = 1010, M = 15;
char str[N][M];
int f[M][M];
int solve(char a[], char b[])
{
    int l1 = strlen(a + 1), l2 = strlen(b + 1);
    for(int i = 1; i <= l1; i++) f[i][0] = i;
    for(int j = 1; j <= l2; j++) f[0][j] = j;
    for(int i = 1; i <= l1; i++)
    {
        for(int j = 1; j <= l2; j++)
        {
            f[i][j] = min(f[i - 1][j], f[i][j - 1]) + 1;
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
    }
    return f[l1][l2];
}
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++)
    {
        scanf("%s", str[i] + 1);
    }
    char s[M];
    int limit;
    for(int j = 1; j <= m; j++)
    {
        scanf("%s%d", s + 1, &limit);
        int res = 0;
        for(int i = 1; i <= n; i++)
        {
            res += (solve(str[i], s) <= limit);
        }
        printf("%d\n", res);
    }
    return 0;
}

4. 状态机模型

1057. 股票买卖 IV

  • 给定一个长度为 N 的数组,数组中的第 i i i 个数字表示一个给定股票在第 i i i 天的价格。设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。

  • f ( i , j , k ) f(i, j, k) f(i,j,k):表示第 i i i 天,已经进行了 j 次交易,此时的状态为 k 时的收益最大值。 k = 0 k=0 k=0 表示手中没有股票, k = 1 k=1 k=1表示手中有股票。

  • 两个状态,四条有向边,分别对应四个状态转移:

    1. f ( i , j , 0 ) = m a x { f ( i − 1 , j , 0 ) , f ( i − 1 , j , 1 ) + w [ i ] } f(i, j, 0) = max\{f(i - 1, j, 0), f(i - 1, j , 1) + w[i]\} f(i,j,0)=max{f(i1,j,0),f(i1,j,1)+w[i]}

    2. f ( i , j , 1 ) = m a x { f ( i − 1 , j , 1 ) , f ( i − 1 , j − 1 , 0 ) − w [ i ] } f(i, j, 1) = max\{f(i - 1, j, 1), f(i - 1, j - 1, 0) - w[i]\} f(i,j,1)=max{f(i1,j,1),f(i1,j1,0)w[i]}

  • 关于初始化的问题。如果不初始化为 − I N F -INF INF,那么交易了 u 次后,也许收益会变成负数,但是状态会从 0 转移过来。 根据实际情况,什么时候收益会是0呢?首先,交易了0次时,收益一定是0,而此时手中一定没有股票。其次,在每一天,都不交易股票,那么收益仍然是0。因此,初始化应是 f ( i , 0 , 0 ) = 0 , 0 ≤ i ≤ N f(i, 0, 0) = 0, 0 \le i \le N f(i,0,0)=0,0iN.

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010, maxm = 110;
int N, M, w[maxn], f[maxn][maxm][2];
int main() {
	scanf("%d%d", &N, &M);
	//因为状态会有负数,所以要初始化为负无穷。
	memset(f, -0x3f, sizeof f);
	//这个地方初始化注意!
	for (int i = 0; i <= N; i++) f[i][0][0] = 0;

	for (int i = 1; i <= N; i++) scanf("%d", &w[i]);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= M; j++) {
			f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + w[i]);
			f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0] - w[i]);
		}
	}
	int ans = 0;
	for (int j = 1; j <= M; j++) ans = max(f[N][j][0], ans);
	printf("%d\n", ans);
	return 0;
}

1058. 股票买卖 V

  • 给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

  • 这个就是此题的状态机:
    在这里插入图片描述

  • 于是, f ( i , j ) f(i, j) f(i,j)表示在第 i i i 天,状态为 j j j 时,收益最大值。 j = 0 j=0 j=0 表示手中有货, j = 1 j=1 j=1表示手中无货第一天, j = 2 j=2 j=2 表示手中无货 ≥ 2 \ge2 2 天。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010, INF = 0x3f3f3f3f;
int f[maxn][3], w[maxn], N;
int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) {
		scanf("%d", &w[i]);
	}
	memset(f, -0x3f, sizeof f);
	//把入口初始化为0
	f[0][2] = 0;
	for (int i = 1; i <= N; i++) {
		f[i][0] = max(f[i - 1][0], f[i - 1][2] - w[i]);
		f[i][1] = f[i - 1][0] + w[i];
		f[i][2] = max(f[i - 1][2], f[i - 1][1]);
	}
	printf("%d\n", max(f[N][1], f[N][2]));
	return 0;
	
}

5. 状态压缩 DP

6. 区间 DP

320. 能量项链

  • 题意:给 n n n 个珠子排成一个环,每次可以挑选一颗珠子,把它拿走(假设拿走了第 i i i 颗石子),释放的能量是 a i − 1 ∗ a i ∗ a i + 1 a_{i-1}*a_i*a_{i+1} ai1aiai+1. 问获得的最大的能量值是多少. 注意这个题最后要合并成一个珠子,如果最后剩两个珠子 a i a_i ai a j a_j aj,那么释放的能量将是 a i ∗ a j ∗ a i a_i * a_j * a_i aiajai 或者 a j ∗ a i ∗ a j a_j * a_i * a_j ajaiaj.
#include<bits/stdc++.h>
using namespace std;
const int N = 210;
int n;
int f[N][N], a[N];
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        a[i + n] = a[i];
    }

    //长度从3开始枚举,到 n + 1
    for(int len = 3; len <= n + 1; len++)
    {
        for(int l = 1, r = l + len - 1; r <= 2 * n; l++, r++)
        {
            //转移节点不能取到左右边界
            for(int k = l + 1; k < r; k++)
            {
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + a[l] * a[k] * a[r]);
            }
        }
    }
    
    int res = 0;
    // 答案对应的区间长度是 n + 1.
    for(int l = 1, r = l + n; r <= 2 * n; l++, r++)
    {
        res = max(res, f[l][r]);
    }
    printf("%d\n", res);
    return 0;
}

1069. 凸多边形的划分

  • 题意:给定一个具有 N N N 个顶点的凸多边形,将顶点从 1 1 1 N N N 标号,每个顶点的权值都是一个正整数。将这个凸多边形划分成 N − 2 N−2 N2 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。 N ≤ 50 N \le 50 N50,结点权值均小于 1 0 9 10^9 109.
  • 可以这样定义。 f ( l , r ) f(l,r) f(l,r) 为从 v l , v l + 1 , v l + 2 , . . . , v r v_l,v_{l+1},v_{l+2},...,v_r vl,vl+1,vl+2,...,vr 这些点组成的多边形的最小权值和。我们假设中转节点是 k ( l < k < r ) k(l < k <r) k(l<k<r),那么相当于拆成了 f ( l , k ) , f ( k , r ) , △ v l v k v r f(l,k),f(k,r),\triangle v_lv_kv_r f(l,k),f(k,r),vlvkvr.
  • 因此这道题实际上是在枚举 v l v r v_lv_r vlvr 这条边以及中转结点 v k v_k vk. 无需将链扩展为两倍就可以写,因为 f ( 1 , n ) f(1,n) f(1,n) 已经包含了每一条边. 不过下面的代码还是按照链扩展来写的.
    在这里插入图片描述
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
const __int128 INF = 1e36;
__int128 f[N][N];
int a[N], n;
void Print(__int128 x)
{
    if(x < 0) 
    {
        putchar('-');
        x = -x;
    }
    if(x > 9) Print(x / 10);
    putchar(x % 10 + '0');
    
}
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        a[i + n] = a[i];
    }
    
    
    for(int len = 3; len <= n; len++)
    {
        for(int l = 1, r = l + len - 1; r <= 2 * n; l++, r++)
        {
            //在这里初始化f更方便
            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] + (__int128)a[l] * a[k] * a[r]);
            }
        }
    }
    
    __int128 res = INF;
    for(int l = 1, r = l + n - 1; r <= 2 * n; l++, r++)
    {
        res = min(res, f[l][r]);
    }
    Print(res);
    return 0;
}

479. 加分二叉树

设一个 n n n 个节点的二叉树 tree 的中序遍历为( 1 , 2 , 3 , … , n 1,2,3,…,n 1,2,3,,n),其中数字 1 , 2 , 3 , … , n 1,2,3,…,n 1,2,3,,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i i i 个节点的分数为 d i d_i di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:

subtree的左子树的加分 × × × subtree的右子树的加分 + + subtree的根的分数

**若某个子树为空,规定其加分为 1 1 1。**叶子的加分就是叶节点本身的分数,不考虑它的空子树。试求一棵符合中序遍历为( 1 , 2 , 3 , … , n 1,2,3,…,n 1,2,3,,n)且加分最高的二叉树 tree。

要求输出:(1)tree的最高加分(2)tree的前序遍历。

f ( l , r ) f(l,r) f(l,r) 表示从中序遍历为 l l l r r r 且加分最大的子树的加分值. 因为不改变中序遍历,因此同一棵子树的结点编号一定是连起来的. 那么我们只需要枚举子树的根节点,即 min ⁡ l < k < r { f ( l , k − 1 ) ∗ f ( k + 1 , r ) + w [ k ] } \min\limits_{l < k < r}\{f(l, k - 1)*f(k + 1,r) + w[k]\} l<k<rmin{f(l,k1)f(k+1,r)+w[k]}. 不过需要注意边界问题,由于子树可能为空,但是子树为空的权值为1,因此需要特殊处理.

#include<bits/stdc++.h>
using namespace std;
const int N = 35, INF = 0x3f3f3f3f;
int f[N][N], g[N][N], w[N];
int n;

void dfs(int l, int r)
{
    int root = g[l][r];
    printf("%d ", root);
    if(l <= root - 1) dfs(l, root - 1);
    if(root + 1 <= r) dfs(root + 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, r = l + len - 1; r <= n; l++, r++)
        {
            //子树大小为1的时候需要注意
            if(l == r) 
            {
                f[l][r] = w[l];
                g[l][r] = l;
            }
            else for(int k = l; k <= r; k++)
            {
                int left = (k == l) ? 1 : f[l][k - 1];
                int right = (k == r) ? 1 : f[k + 1][r];
                int score = left * right + w[k];
                if(f[l][r] < score)
                {
                    f[l][r] = score;
                    g[l][r] = k;
                }
            }
        }
    }
    
    printf("%d\n", f[1][n]);
    
    dfs(1, n);
    return 0;
}

321. 棋盘分割

在这里插入图片描述

  • 二维区间 DP 问题. 定义 f ( x 1 , y 1 , x 2 , y 2 , k ) f(x_1,y_1,x_2,y_2,k) f(x1,y1,x2,y2,k) 为 左上角 ( x 1 , y 1 ) (x_1,y_1) (x1,y1),右下角 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 的矩形切 k k k 刀后均方差最小值。 二维区间 DP 由于状态太多,迭代不好写,因此写为记忆化搜索

由于 x ‾ \overline{x} x 是定值,因此我们只需要让 ∑ i = 1 n ( x i − x ‾ ) 2 n \frac{\sum\limits_{i=1}^n (x_i - \overline x)^2}{n} ni=1n(xix)2 最小即可。对于每一个 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1),(x_2,y_2) (x1,y1),(x2,y2) 的矩形,横着有 x 2 − x 1 − 1 x_2 - x_1 - 1 x2x11 的种方法,可以选择 2 ∗ ( x 2 − x 1 − 1 ) 2 * (x_2 - x_1 - 1) 2(x2x11) 种方案(扔掉上面的或者下面的),然后被扔掉的那一块儿通过二位前缀和可以迅速求得. 对于列的切割是同理的.

#include<bits/stdc++.h>
using namespace std;
const int N = 15, M = 9, INF = 1e9;
double f[M][M][M][M][N];
int sum[M][M], n;
double X;

double get(int x1, int y1, int x2, int y2)
{
    //X是浮点数,因此 s 一定不要写成 int
    //这里公式没有错,想清楚了
    double s = sum[x2][y2] - sum[x2][y1 - 1] - sum[x1 - 1][y2] + sum[x1 - 1][y1 - 1] - X;
    return s * s / n;
}

double dp(int x1, int y1, int x2, int y2, int k)
{
    double& v = f[x1][y1][x2][y2][k];

    if(v >= 0) return v;
    if(k == 1) return get(x1, y1, x2, y2);

    v = INF;

    for(int i = x1; i < x2; i++)
    {
        v = min(v, dp(x1, y1, i, y2, k - 1) + get(i + 1, y1, x2, y2));
        v = min(v, get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1));
    }

    for(int j = y1; j < y2; j++)
    {
        v = min(v, dp(x1, y1, x2, j, k - 1) + get(x1, j + 1, x2, y2));
        v = min(v, get(x1, y1, x2, j) + dp(x1, j + 1, x2, y2, k - 1));
    }
    return v;
}

int main()
{
    scanf("%d", &n);
    memset(f, -1, sizeof f);
    for(int i = 1; i <= 8; i++)
    {
        for(int j = 1; j <= 8; j++)
        {
            scanf("%d", &sum[i][j]);
            //求二位前缀和小心别弄错.
            sum[i][j] += sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
        }
    }
    X = (double)sum[8][8] / n;
    printf("%.3f\n", sqrt(dp(1, 1, 8, 8, n)));
    return 0;
}

7.树形DP

1075. 数字转换

  • 题意:如果一个数 x x x 的约数之和 y y y(不包括他本身)比他本身小,那么 x x x 可以变成 y y y y y y 也可以变成 x x x。例如, 4 4 4 可以变为 3 3 3 1 1 1 可以变为 7 7 7。限定所有数字变换在不超过 n n n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。 n ≤ 50000 n \le 50000 n50000
  • 这个题形成的是一个可能不连通的无向无环图,但是从一个结点出发往下走,就可以形成一个树的结构. 因此可以相当于找到几棵树的直径的最大值.
  • 至于怎样快速求约数之和。想想怎么样快速筛出所有约数的?
#include<bits/stdc++.h>
using namespace std;
const int N = 50010, M = N * 2;
int f[N];
int h[N], e[M], ne[M], idx;
int n, st[N], ans;
int cnt[N];
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dfs(int u, int fa)
{
    st[u] = true;
    int d1 = 0, d2 = 0;
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa) continue;
        int d = dfs(v, u) + 1;
        if(d >= d1) d2 = d1, d1 = d;
        else if(d > d2) d2 = d;
    }
    ans = max(ans, d1 + d2);
    return d1;
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
    {
        for(int j = 2 * i; j <= n; j += i)
        {
            f[j] += i;
        }
    }
    memset(h, -1, sizeof h);
    
    for(int i = 1; i <= n; i++)
    {
        if(i > f[i])
        {
            add(i, f[i]);
            add(f[i], i);
        }
    }
    
    for(int i = 1; i <= n; i++)
    {
        if(!st[i]) dfs(i, -1);
    }
    printf("%d\n", ans);
    return 0;
}

1074. 二叉苹果树

  • 题意:有一棵完全二叉树,这棵树共 n ( n ≤ 100 ) n(n \le 100) n(n100) 个节点,编号为 1 1 1 n n n,树根编号一定为 1 1 1。现在要选出给定数量的边 v < 100 v < 100 v<100,使得这些边的权值之和最大,并且边上的结点始终与 1 1 1 号点连通.
  • 实际上,这就是一个有依赖的背包问题. 把边的终点看作物品,边权就是物品价值,选择的边的数量当作背包容量。然后是容量恰好为 v v v.
#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = N * 2;

int h[N], e[M], ne[M], w[M], idx;
int n, v, value[N];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

int f[N][N], sz[N];
int dfs(int u, int fa)
{
    sz[u] = 1;
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int son = e[i];
        if(son == fa) continue;
        value[son] = w[i];
        sz[u] += dfs(son, u);
        //从大到小循环
        for(int j = min(sz[u] - 1, v - 1); j >= 0; j--)
        {
            for(int k = j; k >= 0; k--)
            {
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
            }
        }
    }
    //一定要从大到小循环
    for(int j = v; j >= 1; j--)
    {
        f[u][j] = f[u][j - 1] + value[u];
    }

    f[u][0] = 0;
    return sz[u];
}

int main()
{
    scanf("%d%d", &n, &v);
    v++;
    memset(h, -1, sizeof h);
    memset(f, -0x3f, sizeof f);
    for(int i = 1; i <= n; i++) f[i][0] = 0;

    for(int i = 1; i < n; i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }

    dfs(1, -1);
    printf("%d\n", f[1][v]);

    return 0;
}

323. 战略游戏

  • 他必须保护一座中世纪城市,这条城市的道路构成了一棵树 n ≤ 1500 n \le 1500 n1500。每个节点上的士兵可以观察到所有和这个点相连的边。他必须在节点上放置最少数量的士兵,以便他们可以观察到所有的边。
  • 这个题的读入很特殊,但是可以采用格式化读入的方式。
  • 然后做法,其实就是和 没有上司的舞会 一样,只是每个节点的权值是1,要最小化答案.
#include<bits/stdc++.h>
using namespace std;
const int N = 1510, M = N * 2;
int h[N], e[M], ne[M], idx;
int n;
int st[N], f[N][2];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u, int fa)
{
    f[u][0] = 0, f[u][1] = 1;
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa) continue;
        dfs(v, u);
        //若不选择u,那么每个v都要选,不然会导致有些边的端点没有选到.
        f[u][0] += f[v][1];
        //选了u之后,v可选可不选.
        f[u][1] += min(f[v][0], f[v][1]);
    }
}

int main()
{
    while(~scanf("%d", &n))
    {
        memset(h, -1, sizeof h);
        idx = 0;
        memset(st, 0, sizeof st);
        for(int i = 1; i <= n; i++)
        {
            int id, cnt;
            scanf("%d:(%d)", &id, &cnt);
            id++;
            while(cnt--)
            {
                int v;
                scanf("%d", &v);
                v++;
                add(id, v), add(v, id);
                st[v] = true;
            }
        }
        dfs(1, -1);
        printf("%d\n", min(f[1][0], f[1][1]));
    }
    return 0;
}

8.数位统计DP

1083. Windy数

  • 题意:不含前导零且相邻两个数字之差至少为 2 的正整数被称为 Windy 数。Windy 想知道,在 A 和 B 之间,包括 A 和 B,总共有多少个 Windy 数?

在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int maxn = 11;
int f[maxn][10];
//f[i][j] 保存的是 i 位最高位的数字是 j 的满足要求的数字的个数。
//这个答案是准确的,至于前导零怎么考虑,是在dp函数里面利用这个算的。
int dp(int n) {
	if (n == 0) return 0;  // 这道题的 0 不在考虑范围内
	vector<int> nums;
	while (n) nums.push_back(n % 10), n /= 10;
	int last = -2, res = 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 += f[i + 1][j];
			}
		}
		if (abs(x - last) >= 2) last = x;
		else break;

		if (i == 0) res++;
	}

	// 特殊处理有前导零的数
	for (int i = 1; i < nums.size(); i++) {
		for (int j = 1; j <= 9; j++) {
			res += f[i][j];
		}
	}
	return res;
}
int main() {
	//预处理
	//这里,应该从0开始枚举。尽管0不在计算之内,但是如果不算0的话,有些状态转移不过来
	//比如,f[2][2],从f[1][0]转移,f[2][2]应该包含20这个数字,如果f[1][0]=0,这个数就转移不过来了
    //但是在算res时,研究dp就会发现,0这个数字是不会算进去的。
	for (int j = 0; j <= 9; j++) f[1][j] = 1;
	for (int i = 2; i < maxn; i++) {
		for (int j = 0; j <= 9; j++) {
			for (int k = 0; k <= 9; k++) {
				if (abs(j - k) >= 2) {
					f[i][j] += f[i - 1][k];
				}
			}
		}
	}
	
	int a, b;
	cin >> a >> b;
	cout << dp(b) - dp(a - 1) << endl;

	return 0;
}

深搜代码

#include<bits/stdc++.h>
using namespace std;
const int N = 35;
int f[N][N][2], a[N];
int A, B;

int dp(int pos, int k, int flag, int lim)
{
    if(pos == -1) return 1;
    if(!lim && f[pos][k][flag] != -1) return f[pos][k][flag];
    int up = lim ? a[pos] : 9;
    int ans = 0;
    for(int i = 0; i <= up; i++)
    {
        if(!flag && abs(i - k) >= 2) ans += dp(pos - 1, i, flag, i == up && lim);
        if(flag) ans += dp(pos - 1, i, i == 0 && flag, i == up && lim);
    }
    if(!lim) f[pos][k][flag] = ans;
    return ans;
}

int solve(int x)
{
    int sz = 0;
    while(x)
    {
        a[sz++] = x % 10;
        x /= 10;
    }
    //千万别忘记初始化
    memset(f, -1, sizeof f);
    return dp(sz - 1, 0, 1, 1);
}

int main()
{
    scanf("%d%d", &A, &B);
    //printf("*** %d\n*** %d\n", solve(A - 1), solve(B));
    printf("%d\n", solve(B) - solve(A - 1));
}

1084. 数字游戏 II

  • 某人又命名了一种取模数,这种数字必须满足各位数字之和 mod N 为 0。现在大家又要玩游戏了,指定一个整数闭区间 [ a , b ] [a,b] [a,b],问这个区间内有多少个取模数。
  • 难点还是在预处理过程.
  • f ( i , j , k ) f(i, j, k) f(i,j,k) 是有 i i i 位,最高位是 j j j ,且模 N 为 k 的数字的数量。 f ( i , j , k ) = ∑ x = 0 9 f ( i − 1 , x , ( k − j )   m o d   N ) f(i, j, k) = \sum\limits_{x=0}^{9} f(i-1, x, (k-j)\ mod\ N) f(i,j,k)=x=09f(i1,x,(kj) mod N).

在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
int f[11][10][110];
int P;
int mod(int x, int y) {
	return (x % y + y) % y;
}
int dp(int n) {
	if (n == 0) return 1;
	vector<int> nums;
	while (n) {
		nums.push_back(n % 10);
		n /= 10;
	}
	int res = 0, last = 0; //last 存左面各分支数字之和。
	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][mod(-last, P)];
		}
		last += x;
		if (i == 0 && (last % P == 0)) res++;
	}
	return res;
}
void init() {
	memset(f, 0, sizeof f);
	for (int j = 0; j <= 9; j++) f[1][j][j % P]++;
	for (int i = 2; i < 11; i++) {
		for (int j = 0; j <= 9; j++) {
			for (int k = 0; k < P; k++) {
				for (int x = 0; x <= 9; x++) {
					f[i][j][k] += f[i - 1][x][mod(k - j, P)];
				}
			}
		}
	}
}
int main() {
	
	int l, r;
	while (cin >> l >> r >> P) {
		init();
		cout << dp(r) - dp(l - 1) << endl;
	}
}

深搜代码

#include<bits/stdc++.h>
using namespace std;
const int N = 35, M = 110;
int f[N][M], a[N];
int A, B, mod;

int dp(int pos, int k, int lim)
{
    if(pos == -1) return k == 0;
    if(!lim && f[pos][k] != -1) return f[pos][k];
    int ans = 0, up = lim ? a[pos] : 9;
    for(int i = 0; i <= up; i++)
    {
        ans += dp(pos - 1, (k + i) % mod, i == up && lim);
    }
    if(!lim) f[pos][k] = ans;
    return ans;
}

int solve(int x)
{
    int sz = 0;
    while(x)
    {
        a[sz++] = x % 10;
        x /= 10;
    }
    memset(f, -1, sizeof f);
    return dp(sz - 1, 0, 1);
}

int main()
{
    ios::sync_with_stdio(0);
    while(cin >> A >> B >> mod)
    {
        cout << solve(B) - solve(A - 1) << endl;
    }
}

J. Junior Mathematician

  • 感觉这个题又是一个 d p dp dp 的板子,有时候用大雪菜的方法并不好处理状态不含数位的情况。就比如这个,不含第 i 位数字是几的信息,因此没办法处理后面的步骤。
  • 题意:求 [ L , R ] ( R ≤ 1 0 5000 ) [L,R] (R \le 10^{5000}) [L,R](R105000) 间满足 x ≡ f ( x )   m o d   m ( m ≤ 60 ) x \equiv f(x) \bmod m (m \le 60) xf(x)modm(m60) 的数量。 f ( x ) f(x) f(x) 表示所有数位两两之积的和。
  • d p ( i , s u m , r e s ) dp(i, sum, res) dp(i,sum,res):第 i i i 位,当前数字之和是 s u m sum sum,当前 f ( x ) − x f(x) - x f(x)x r e s res res
  • 这样子的话,我们用深搜去搜索答案。和大雪菜的思路差不多,从最高位开始搜, d f s dfs dfs 中间那一大块儿,不同的数位 d p dp dp 都是一样的。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>

typedef long long ll;
const ll mod = 1e9 + 7;
using namespace std;
const int maxn = 5010;
char s1[maxn], s2[maxn];
int M;
int f[maxn][60][60], p[maxn], a[maxn];
int my_mod(int a, int b) {
	return (a % b + b) % b;
}

int dp(int pos, int sum, int res, bool limit) {
	// lim=1 表示当前贴合上界,lim=0 则不贴合
	if (pos == -1) {
		return res == 0;
	}
	if (limit == false && f[pos][sum][res] != -1) {
		return f[pos][sum][res];
	}
	else {
		int ans = 0, up = limit ? a[pos] : 9;
		for (int i = 0; i <= up; i++) {
			int xx = my_mod(res + sum * i - i * p[pos], M);
			ans = (ans + dp(pos - 1, my_mod(sum + i, M), xx, (i == up) && limit)) % mod;
		}
		if (limit == false) {  // 不贴合上界的情况有可能会被复用
			f[pos][sum][res] = ans;
		}
		return ans;
	}
}

int solve(char s[], int n) {
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < M; j++) {
			memset(f[i][j], -1, sizeof f[i][j]);
		}
	}
	for (int i = 0; i < n; i++) {
		a[i] = s[n - i - 1];
	}
	//这个方法也是从高数位往低数位填数字
	return dp(n - 1, 0, 0, 1);
}

int main() {
	int T;
	scanf("%d", &T);

	while (T--) {
		scanf("%s%s%d", s1, s2, &M);
		int n1 = strlen(s1), n2 = strlen(s2);

		for (int i = 0; i < n1; i++) s1[i] -= '0';
		for (int i = 0; i < n2; i++) s2[i] -= '0';
		s1[n1 - 1]--;
		for (int i = n1 - 1; i >= 0; i--) {
			if (s1[i] < 0) {
				s1[i] += 10;
				s1[i - 1]--;
			}
			else break;
		}

		//for (int i = 0; i < n1; i++) printf("%d", s1[i]);

		p[0] = 1;
		for (int i = 1; i < maxn; i++) {
			p[i] = p[i - 1] * 10 % M;
		}
		//printf("*** %d %d\n", solve(s2, n2), solve(s1, n1));
		printf("%d\n", my_mod(solve(s2, n2) - solve(s1, n1), mod));
	}
	return 0;
}

D. Beautiful numbers

  • 再来一道深搜处理数位 d p dp dp 的题目。

  • 题意:Beautiful Numbers定义为这个数能整除它的所有位上非零整数。问[l,r]之间的Beautiful Numbers的个数。

  • 数位 DP: 数位 DP 问题往往都是这样的题型,给定一个闭区间 [ l , r ] [l,r] [l,r],让你求这个区间中满足某种条件的数的总数。我们将问题转化成更加简单的形式。设 a n s i ans_i ansi 表示在区间 中满足条件的数的数量,那么所求的答案就是 a n s r − a n s l − 1 ans_{r} - ans_{l - 1} ansransl1.

  • 看代码吧,可以发现所有个位数的最小公倍数是2520,dfs(len, sum, lcm, limit) ,len表示迭代的长度,sum为截止当前位的数对2520取余后的值。 lcm为截止当前位的所有数的最小公倍数。limit表示当前数是否可以任意取值(对取值上限进行判断).

  • 代码其实很好懂,但是方法确实不是那么好想。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxlcm = 2520;
typedef long long ll;
ll dp[20][50][2530];
int Hash[2530];
ll nums[20];
ll gcd(ll a, ll b) {
	if (b == 0) return a;
	return gcd(b, a % b);
}
ll dfs(ll pos, ll sum, ll lcm, bool limit) {
	if (pos == -1) {
		return sum % lcm == 0;
	}
	if (limit == false && dp[pos][Hash[lcm]][sum] != -1) return dp[pos][Hash[lcm]][sum];
	ll ans = 0;
	int up = limit ? nums[pos] : 9;
	for (int i = 0; i <= up; i++) {
		ans += dfs(pos - 1, (sum * 10 + i) % maxlcm, i ? i * lcm / gcd((ll)i, lcm) : lcm, limit && i == up);
	}
	if (limit == 0) dp[pos][Hash[lcm]][sum] = ans;
	return ans;
}
ll solve(ll N) {
	int p = 0;
	while (N) {
		nums[p++] = N % 10;
		N /= 10;
	}
	return dfs(p - 1, 0, 1, true);
}
int main() {
	int T;
	scanf("%d", &T);
	memset(dp, -1, sizeof dp);
	int cnt = 0;
	for (int i = 1; i <= maxlcm; i++) {
		if (maxlcm % i == 0) Hash[i] = cnt++;
	}
	while (T--) {
		ll l, r;
		cin >> l >> r;
		cout << solve(r) - solve(l - 1) << endl;
	}
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/634034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023夏-PAT甲级题解

目录 总结&#xff1a; A-1 Trap Input Specification: Output Specification: Sample Input: Sample Output: 题意&#xff1a; 思路&#xff1a; AC代码&#xff1a; A-2 Queue Using Two Stacks Input Specification: Output Specification: Sample Input: Sa…

SpringBoot 项目部署笔记

1. 直接通过 jar 包部署 本地直接 build package 成 jar 包&#xff0c;上传至服务器 ps -ef|grep XXX #查找项目进程sudo kill -9 19727 #杀掉项目进程nohup sudo java -jar *.jar >> app.log & #后台运行 jar &代表让该命令在后台执行 3. 通过 Jenkins …

pytorch笔记:conv2d

来自B站视频&#xff0c;API查阅&#xff0c;TORCH.NN nn.conv2d 中一般 kernel_size 是小奇数&#xff0c;padding 设置为 k − 1 2 \frac{k-1}{2} 2k−1​&#xff08;实际上padding的是 k − 1 k-1 k−1&#xff0c;因为参数的意义是左右各padding&#xff09;&#xff0c;

探索Xiotman:物联网软件架构的创新之路

文章目录 探索Xiotman&#xff1a;物联网软件架构的创新之路什么是物联网什么是XiotmanXiotman的特点Xiotman的架构Xiotman的使用安装env工具获取源代码使用其他教程 Xiotman的开源地址 总结 探索Xiotman&#xff1a;物联网软件架构的创新之路 什么是物联网 &#x1f680;&am…

高燃盛会全程回顾|鸿雁加速推进数字转型之路

6月10日&#xff0c;以“双翅齐振雁南飞”为主题的鸿雁电器数字化营销启动大会暨中山古镇鸿雁全屋智能体验中心开业庆典&#xff0c;在中山古镇华艺广场圆满落幕。 古镇镇长阮志力、华艺集团董事长区锦标、华艺广场总经理丁瑜、古镇灯饰传媒董事长曹利晖以及杭州鸿雁电器有限公…

spring boot + xxl-job 分布式任务调度

一、介绍 1、任务调度 1.1、什么是任务调度 我们可以先思考一下下面业务场景的解决方案&#xff1a; 某电商系统需要在每天上午10点&#xff0c;下午3点&#xff0c;晚上8点发放一批优惠券。某财务系统需要在每天上午10点前结算前一天的账单数据&#xff0c;统计汇总。某电…

那年我头脑发热,选择了自动化,后来我掉入计算机的世界无法自拔

首先&#xff0c;小雅兰是22届高考考生&#xff0c;而且当时填报志愿也没有填报到计算机相关的专业去&#xff0c;小雅兰是自动化专业的学生&#xff0c;是由于一次偶然的机会&#xff0c;了解到了这个行业&#xff0c;对于写代码所带来的成就感&#xff0c;总之&#xff0c;我…

Java013——常见进制以及转换

一、常见进制 十进制 数字组成&#xff1a;0-9这十个数字组成&#xff0c;不能以0开头 进位规则&#xff1a;满10进1 int num1 10;//对应的十进制为10二进制 数字组成&#xff1a;0-1这两个数字组成&#xff0c;以0b或0B开头 进位规则&#xff1a;满2进1 int num2 0b1010;…

华为存储IA篇仿真器搭建

设备清单 编号 设备名 数量 备注 01 Windows系统主机 1台 为VMware提供安装位置 02 VMware软件 1份 提供存储仿真器的部署环境 03 仿真器文件 1份 用于部署estor虚拟机 【注意】&#xff1a;暂无注意事项 一、下载安装文件并配置虚拟机设备清单 1.1…

TypeScript 自定义装饰器

&#xff08;预测未来最好的方法就是把它创造出来——尼葛洛庞帝&#xff09; 装饰器 装饰器一种更现代的代码模式&#xff0c;通过使用的形式注入在属性&#xff0c;寄存器&#xff0c;方法&#xff0c;方法参数和类中&#xff0c;比如在Angular&#xff0c;Nestjs和midway等…

大数据Doris(三十九):Spark Load 注意事项

文章目录 Spark Load 注意事项 Spark Load 注意事项 1、现在Spark load 还不支持 Doris 表字段是String类型的导入,如果你的表字段有String类型的请改成varchar类型,不然会导入失败,提示 type:ETL_QUALITY_UNSATISFIED; msg:quality not good enough to cancel 2、使用Spa…

无敌!我用【C语言】手搓出了一个体系完整的【员工管理系统】还能玩游戏听音乐?(超详细,附完整源码)

博主简介&#xff1a;Hello大家好呀&#xff0c;我是陈童学&#xff0c;一个与你一样正在慢慢前行的人。 博主主页&#xff1a;陈童学哦 所属专栏&#xff1a;C语言程序设计实验项目 如果本文对你有所帮助的话&#xff0c;还希望可以点赞&#x1f44d;收藏&#x1f4c2;支持一下…

Linux——IP协议1

目录 协议头格式 如何封装和解包 如何交付&#xff08;分用&#xff09; 报头每一个字段 分片是怎么做到的 应用层解决的是数据使用的问题。 在传输层&#xff0c;网络层&#xff0c;数据链路层&#xff1a;解决的是网络通信的细节&#xff0c;将数据可靠的从A主机跨网络发…

【深入浅出 Spring Security(八)】前后端分离-使用CSRF漏洞保护详讲

CSRF 漏洞保护 一、CSRF 概述二、CSRF 攻击演示三、CSRF 防御令牌同步模式 四、前后端分离使用 CSRFCsrfFilter 源码分析源码一些方法的细究 测试 五、总结 一、CSRF 概述 CSRF&#xff08;Cross-Site Request Forgery 跨站请求伪造&#xff09;&#xff0c;也可称为一键式攻击…

乐盟互动申请纳斯达克IPO上市,募资2000万美元

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;来自北京的程序化广告平台【乐盟互动】近期已向美国证券交易委员会&#xff08;SEC&#xff09;提交招股书&#xff0c;申请在纳斯达克IPO上市&#xff0c;股票代码&#xff08;LIAI&#xff09;&…

SpringBoot社区小区物业管理停车场系统(Java+Layui+MyBatis+Python+Mysql)

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;69小区 获取完整源码源文件说明文档数据库文件 项目特色 本项目使用现行主流技术与架构模式&#xff08;控制层、服务层、数据层&#xff09;代码结构清晰&#xff0c;严格遵循模块化、组件化、接口化思想&#xff1b;关…

mysq在RR级别怎么解决不可重复读和幻读

1、定义 不可重复读&#xff1a; 事务1读取一行&#xff0c;事务2然后修改或删除该行数据并且提交事务&#xff0c;事务1再次读取结果不一样&#xff1b; 幻读&#xff1a;事务1按条件读取查询数据&#xff0c;事务2按照同样的条件新增一条或多条数据并且提交事务&#xff0c…

mysql8查看大事务

文章目录 1、查看大事务的原因2、构建测试数据3、模拟大事务场景4、查询mysql的事务5、查询大事务的详情 1、查看大事务的原因 大事务的特点是执行时间长&#xff0c;长期占有锁不释放&#xff0c;导致其他想操作同一行数据的线程阻塞&#xff0c;如果客户端设置了超时时间&am…

单正态总体和双正态总体的假设检验

1.单正态总体和双正态总体的假设检验 笔者之前的相关笔记&#xff1a; 1.正态总体下常见的抽样分布 2.假设检验&#xff08;Hypothesis Testing&#xff09; 个人理解假设检验&#xff1a;先对总体参数提出一个假设值&#xff0c;利用样本信息判断这一假设是采取拒绝该假设还是…

opencv人与摄像头距离检测

参考&#xff1a; https://chtseng.wordpress.com/2018/09/18/%E5%A6%82%E4%BD%95%E4%BC%B0%E7%AE%97%E5%89%8D%E6%96%B9%E4%BA%BA%E7%89%A9%E7%9A%84%E8%B7%9D%E9%9B%A2/ WeChat_20230611160620 1、cv2加载摄像头慢解决方法&#xff0c;单独重新cv2.VideoCapture() https://b…