11.30
过去的5分钟收获了什么?(回答不上来感觉回头复习)未来的一个小时将会收获什么?
题目描述:
给定一个正整数 N 和长度为 10 的数组 A0,A1,A2,...,A9,请求满足以下条件的数的个数:
-
长度不超过 N
-
数位 i 的出现次数至少为 A_i
-
无前导 0
n<=100,A_i<=100
日期、用时:11.30 1e24(思考)+22(代码)+1h6min(总结)
解法:
1.涉及的算法,解题方法?
dp,组合数学。
2.难点?突破口?本质?
最初的思路肯定是直接组合数学算或者考虑朴素dp。
直接组合数学算是不容易的,因为位数是不确定的。
考虑dp。最朴素的dp设10个维度。把每个数字用了几个都记下来。超时。
通过观察可以发现:(难点,突破口)
性质:任意数字本质上没有区别。以任意顺序满足即可。
所以从0到9满足,考虑dp。
难点2:处理前导零。
方法:枚举最高位,将最高位钦定为一个非0数。
难点3:(优化时间复杂度)是dp状态顺序对时间复杂度的影响。
dp状态选择合适的顺序可以省略某些变量的枚举。
一开始的做法O(10*10n^3)可以通过
我去除前导零的方法是枚举位数d和最高位的数字。
设dp_{i,j}用数字[0,i],还剩下j个位置可以用的方案数。
dp转移是容易的,乘一个组合数即可。
优化1(看题解):O(10*10n^2)
无需枚举位数,直接
设dp_{i,j}用数字[0,i],填j个位置的方案数。
转移方法是类似的。
优化2:O(10n^2)
设dp_{i,j}用数字[i,9],填j个位置的方案数。
乘组合数时在i=0时将可以选的位置减少1即可。因为不能填最高位。
1.如果没做出来或做得慢,为什么?卡壳点在哪?
一开始没有采取合适的方法计数。对于求所有约束的补集都同时满足时的答案可以快速计算才应考虑容斥。
去除前导零影响的方法不熟练;设置合适的dp状态定义的顺序从而省略枚举一些值的方法未掌握。
2.为什么用这种解法,这种解法基于什么特征?
存在多个约束需要多次决策满足;数据范围dp可以承受,一般不会太大。
3.在算法应用,解题方法(技巧),思考方法方面有什么收获?
思路选取:对于求所有约束的补集都同时满足时的答案可以快速计算才应考虑容斥。
计数问题中处理前导零的方法:钦定最高位的数不为零。
省略某个值的枚举:需要枚举的值在dp状态中作为某个状态变量;初值为d-i相关的数。
具体的方法是将dp状态中关于这个值的状态变量的定义取反。
4.代码是否效率够高,能否快速写对代码?(如果不能,请对标高手)
rng_58
ll func(int init){ int i,j,k; int prev = a[init]; if(a[init] != 0) a[init]--; REP(i,20) REP(j,110) dp[i][j] = 0; dp[0][0] = 1; REP(i,10) REP(j,N+1) if(dp[i][j] != 0) for(k=a[i];j+k<=N;k++) dp[i+1][j+k] = (dp[i+1][j+k] + dp[i][j] * C[j+k][k]) % MOD; a[init] = prev; ll ans = 0; REP(i,N+1) ans = (ans + dp[10][i]) % MOD; return ans; } int main(void){ int i,j; cin >> N; REP(i,10) cin >> a[i]; N--; REP(i,110) REP(j,i+1){ if(j == 0 || j == i) C[i][j] = 1; else C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD; } ll ans = 0; for(i=1;i<=9;i++) ans = (ans + func(i)) % MOD; cout << ans << endl; return 0; }
5.将题目变形是否还能做,类似的(经典)题有什么共同的解法和思路?对于这题相关的一类问题本质上有什么共同之处?
同样是通过调整dp状态定义的顺序而省略原来需要枚举的值。
本质上和这题都具有要枚举的值在状态变量中存在的特征。
拓展:
组合计数dp的常见技巧:
1.去重:极端性原理:只在最极端(大、小)出计数。
2.当dp过程中中含有必要枚举的变量时,可以考虑改变dp状态定义顺序从而可以省去dp过程中枚举某个量,
仅需在最后统计答案时枚举即可。
Almost Arithmetical Progression
题目描述:
先给出一个整数 n ,再给出一个有 n 个元素的序列 b。
现在要你求序列 b 中最长的子序列,满足隔位的两个数相等,问这个最长的子序列的长度是多少。
n<=4000,b<=1e6
日期,用时:11.30 10min
解法:
dp_{i,j} i为结尾的前缀中,最后一个数是a_i,前面一个数是a_j的最长满足要求的子序列的长度。
朴素转移O(n)
核心:注意到对于每一种j的转移,dp_{j,k}的a_i=a_k,这里的k越靠后越优,转移只需要考虑最靠后的即可。
O(1)转移。
用这些解法基于什么特征?有什么收获?
朴素dp超时,可以考虑找那些转移或者状态才可能成为最优解,只需要考虑可能成为最优解的即可。(找最优解的性质)
CF1787C
题意: 有一个由 n 个整数 a_1,a_2, \ldots, a_n 和一个整数 s 组成的数列 a 。对于每个 a_2,a_3, \ldots, a_{n-1} ,他选择了一对非负整数 x_i 和 y_i ,使得 x_i+y_i=a_i 和 (x_i-s) \cdot (y_i-s) \geq 0 .
$$
F = a_1 \cdot x_2+y_2 \cdot x_3+y_3 \cdot x_4 + \ldots + y_{n - 2} \cdot x_{n-1}+y_{n-1} \cdot a_n.
$$
请帮他找出通过最优选择 x_i 和 y_i 可以得到的最小值 F 。可以证明至少有一种有效的选择方法。
求这个式子的值也是使用类似的方法只考虑可能成为最优解的状态,从而得到只可能在最大,最小值取值的性质。优化dp。
过去的5分钟收获了什么?(回答不上来感觉回头复习)未来的一个小时将会收获什么?
题目描述:
有n种不同的硬币,每个硬币有一个价值a_i(不同硬币价值可能相同).
Bessie有若干种选择硬币的方法使得选出的硬币总价值为t.Bessie会给你q对(b,c),并告诉你第b种硬币的数量严格大于第c种硬币的数量.保证这q对(b,c)中所有b都不同且所有c也都不同。 问有多少种选择硬币的方法满足选出的硬币总价值为t?
Bessie认为两种方案不同指的是两种方案存在至少一种硬币数量不同。
答案对10^9+7取模。
日期、用时:11.30 33(思考)+20(看题解)+13(代码)+34(总结)
解法:
涉及的算法,解题方法?
难点?突破口?本质?
首先,偏序关系先建图b->c,容易看出不是环就是链,环一定不合法。
问题变成对于每一条链满足数量偏序关系的背包问题。
直接朴素dp要存上一个的值O(n1e5^2)
难点是处理这里的下界。如何在省略存上一个点的值的情况下满足偏序要求。
问题的下界为叶子根的值可以为0,叶子的值为链长-1。
即
1->2->3->4
3 2 1 0
满足下界的解才合法。
把下界的花费减掉。
问题变成了每个点下界为0。
现在考虑如果一个点取值+1,还保持正确的偏序关系如何处理。
对于前驱的任意合法取值,把路径上的每个前驱的取值都+1即可。
可以用完全背包解决。
核心难点是将问题转换为无下界问题和每个点取值时保持偏序关系仍然成立。
1.如果没做出来,为什么?卡壳点在哪?
卡壳点在不会处理偏序关系的方法。
2.为什么用这种解法,这种解法基于什么特征?
偏序关系——>建图。
3.在算法应用,解题方法(技巧),思考方法方面有什么收获?
对于要满足某种偏序关系的问题,一般先求合法的下界。把下界先去掉。
然后对于每一个点的不同决策如何保持偏序关系仍然合法,在dp时考虑对到这个点的路径上的点的任意一种满足偏序关系的策略,如何操作才能保证仍然成立即可。
一般的操作是全局加。
4.代码是否效率够高,能否快速写对代码?(如果不能,请对标高手)
rng_58
REP(i,N) to[i] = -1; REP(i,N) root[i] = true; REP(i,Q){ b[i]--; c[i]--; to[b[i]] = c[i]; root[c[i]] = false; } ll tmp = 0; REP(i,N) if(root[i]){ int sum = 0; int x = i; while(1){ used[x] = true; sum += a[x]; v.push_back(sum); if(to[x] != -1) tmp += sum; if(to[x] == -1) break; x = to[x]; } } REP(i,N) if(!used[i]){ cout << 0 << endl; return 0; } if(tmp > T){ cout << 0 << endl; return 0; } T -= tmp; int M = v.size(); dp[0][0] = 1; REP(i,M) REP(j,T+1){ dp[i+1][j] = dp[i][j]; if(j >= v[i]){ dp[i+1][j] += dp[i+1][j-v[i]]; if(dp[i+1][j] >= MOD) dp[i+1][j] -= MOD; } } cout << dp[M][T] << endl;
5.将题目变形是否还能做,类似的(经典)题有什么共同的解法和思路?对于这题相关的一类问题本质上有什么共同之处?
偏序关系带权也可以做,有向树形图也可以做(对子树全局加)。
方法是类似的,先去下界,再考虑dp时维护偏序仍然成立(任意策略下,对起点到这个点路径上的每一个点保证仍然合法)。
拓展、回顾:
对于含有上下界的计数问题:
去上界的常用方法是容斥。
去下界的常用方法是全局加减,核心是维护好与这个点相关(从起点到这个点的路径)的偏序关系。
题目描述:
如果两个只包含数字且长度为 n的字符串 s和w存在两个数字1≤i,j≤n,使得si<wi,sj>wj*,则称 s 和 w 是不可比的。现在给定两个包含数字和问号且长度为 n 的字符串,问有多少种方案使得将所有问号替换成0到9的数字后两个字符串是不可比的?
日期,用时:11.30 8(思考)+13(代码)+13(总结)
解法:
dp。可以正着做,也可以反着做。
反着做转为求可比的方法数。代码没有细节,cmp写成函数实现简单。
也可以直接用组合数算。
正着做。
dp_{i,0/1,0/1}
前i个位置已经填了,si<wi,sj>wj是否出现的方案数。
这里比较考验代码细节,如果用位运算|或,实现起来很简单。
但是如果if,else判断则很复杂。
用这些解法基于什么特征?有什么收获?
rng_58
dp[0][0] = 1; REP(i,N) REP(j,4){ REP(x,10) if(s[i] == '?' || s[i] == '0' + x){ REP(y,10) if(t[i] == '?' || t[i] == '0' + y){ int j2 = j; if(x > y) j2 |= 1; if(x < y) j2 |= 2; dp[i+1][j2] += dp[i][j]; if(dp[i+1][j2] >= MOD) dp[i+1][j2] -= MOD; } } } cout << dp[N][3] << endl;
当dp中存在是否出现过的状态时,一个好的实现方法是采用逻辑或。
过去的5分钟收获了什么?(回答不上来感觉回头复习)未来的一个小时将会收获什么?
12.1
题目描述:
一个比赛有 n 张比赛门票,门票用随机抽选的方式决定。有 a 个人参与一类抽选,b 个人参与二类抽选,其中一类抽选选中的概率是二类抽选的两倍。共进行 n 次抽选,第 i次随机抽选在前 −1i−1 次没有选出的名额中选出一个人。也就是说,这个人获得了比赛门票,接下来第 +1i+1 到 n 次的抽选集合不包括他。
Dmitry 现在如果要参加门票抽选,请你求出他分别参与一类抽选名额和二类抽选名额时,能够获得比赛门票的概率。(a,b 个人不包括 Dmitry)
日期、用时:12.1 约3h + 30(总结)
解法:
两个是对称的,只考虑Dmitry在A中即可。
dp_{i,j} 前i轮抽选,其中有j个A中的元素被选中,i-j个B中的被选中的概率。
最后统计答案时,相当于求对于A中某一个元素出现在被选中的集合中的概率。所以如果集合中有j个元素,对于A中某个的概率还要乘上j/a
涉及的算法,解题方法?
概率,dp。
难点?突破口?本质?
难点:dp状态定义。
1.如果没做出来,为什么?卡壳点在哪?
一开始设计的dp状态为:dpi,j前i轮中,j个A中被选出,D被选中的概率。
是不可转移的。因为D不是一个特殊的元素,D被选出的概率应该与A中任意一元素被选出的概率一样。所以对于D有没有在这j个中无法确定。
对于这种状态的某个前段\sum_{0=<j<=i} dp[i][j]的和并不为1。这种状态是难以用全概率对于每一种情况都乘上概率加权转移的。
2.为什么用这种解法,这种解法基于什么特征?
多段决策求概率。范围几千dp可以承受。
3.在算法应用,解题方法(技巧),思考方法方面有什么收获?
概率dp对于某个阶段的所有情况的和相加应该为1,这样便于用全概率公式转移。
而不是求满足某种特点条件的概率。
搞清楚题目最终求的事件出现的概率,对于那个元素是特殊的还是一般的。通常情况下都是一般的。
4.代码是否效率够高,能否快速写对代码?(如果不能,请对标高手)
rng_58
int N,A,B; // A: 2, B: 1 double dp[3010][3010]; // round, slot by 2 double func(void){ int i,j; REP(i,N+1) REP(j,i+1) dp[i][j] = 0.0; dp[0][0] = 1.0; REP(i,N) REP(j,i+1) if(dp[i][j] > 1.0E-100){ int a = A-j, b = B-(i-j); dp[i+1][j+1] += dp[i][j] * (2.0*a) / (2.0*a+b); dp[i+1][j] += dp[i][j] * b / (2.0*a+b); } double ans = 0.0; REP(i,N+1) ans += dp[N][i] * i; return ans; } int main(void){ freopen("bonus.in", "r", stdin); freopen("bonus.out", "w", stdout); int n,a,b; cin >> n >> a >> b; A = a+1; B = b; N = min(n, A+B); double ans1 = func() / A; printf("%.12f\n", ans1); A = a; B = b+1; N = min(n, A+B); double ans2 = (N - func()) / B; printf("%.12f\n", ans2);
5.将题目变形是否还能做,类似的(经典)题有什么共同的解法和思路?对于这题相关的一类问题本质上有什么共同之处?
抽象成数学模型:
{x_1...x_n},x取值为0/1,问其中有j个x的取值为0的的概率。
对于每一个x,取值都有不同的权重。
6.对这类算法的本质有什么更深入的理解?
概率dp的本质是,对某一阶段不同决策发生概率,加权的计数问题。