动态规划
动态规划基础
动态规划问题的特征
- 子问题重复
- 最优子结构,无后效性(可以通过扩充状态满足这个特征)
LIS
关于导弹拦截的介绍在这篇笔记里已经有了。
神级技巧!注意lower_bound(begin,end,x)
和upper_bound(begin,end,x)
返回的都是迭代器!这么好的东西,我们怎么能不利用?
可以用*lower_bound(begin,end,x)=x
将数组中第一个大于等于x的数直接更改为x。
LCS
此处LCS指的是最长公共子序列(Longest Common Subsequence)。
如果是两个字符串的LCS,那么只能通过暴力的O(n2) dp解决。
如果是两个排列的LCS,可以转化为LIS从而在O(nlogn)的复杂度内解决。
考虑第二个序列中的每个数bi,建立一个新序列c,ci=pos(bi)=lower_bound(a,a+n,bi)−a,
则序列c的一个上升子序列ci1,ci2,⋯,cim就代表aci1对应bi1等,是一种公共子序列的对应。那么LIS就代表着LCS。
如果对序列的限制改成:每个字符的出现次数为O(1),又怎么解呢?方法是:把一个字符拆成若干个字符来解!
对于某一个字符,如果它在第一个序列中出现多次,就把它拆分为多次出现位置的倒序。如序列a=(1,2,3,1,3,2,4),序列b=(2,1,1,2,3,1,2),则可以把序列b拆为c=(6,2, 4,1, 4,1, 6,2, 5,3, 4,1, 6,2),如2在a中出现的位置为2,6,就拆成6,2。这样做的好处是,如果有c中的一个上升子序列,那么它同样对应着一个公共子序列;倒序保证b中的同一个位置的数字不会对应a中的多个数字,“上升”(而非“不降”)保证a中的同一个位置的数字不会对应b中的多个数字。
我也不记得怎么解了?
树形dp
对于每一条路径,在它的起点和终点的LCA处统计数量。
定义f[i][j](i∈[1,n],j∈{0,1,2})表示以i为根的子树中,到i的距离dis同余于j的点的个数。在dfs到某一个点时,初始化f[x][0]=1,f[x][1]=f[x][2]=0。每搜完一个子节点,就给答案累加上f[ch][0]×f[x][0]+f[ch][1]×f[x][2]+f[ch][2]×f[x][1],再把f[ch]的结果累加到f[x]上(加的顺序要注意,防止重复计数)。
点分治。具体做法考虑中。
中文题面见这里。
到Vjudge提交
首先考虑预处理一些信息。
先证明一个性质:对于每一个s,如果可行的最小b为b1,可行的最大b为b2,那么对于任意k∈N,k∈[b1,b2],(s,k)都是可行的。
证明方法:若b1=b2,显然成立。
否则从最大解中删除一个黑点,在周围寻找一个白点,如果寻找不到,就寻找一个黑点。重复上述过程,不可能一直找不到白点,因为一棵树是连通的,又有b1<b2,因此只要向最小解方向靠拢,就可以找到白点。找到白点后用白点替换删除了的黑点,解的大小即减小1。由于解可以不断地减小直到达到最小答案,因此证明完毕。
证明了这个结论,我们也就只需求出,对于每一个s,可行的b的最大值和最小值分别是多少。而最大值和最小值是对称的,即最大值是黑点最多,最小值是白点最多。因此我们只需要考虑最大值的做法即可。
定义r[i]为i个点的连通块中黑点的最大数目,定义f[i][j]为在以i为根的子树中,包含i的共有j个点的连通块中黑点的最大数目。r[j]=max(f[x][j])。
对树进行dfs,对于某一棵子树的根节点x,先初始化f[x][1]=color[x],size(x)=1。接着对子节点i进行搜索dfs(i,x)
,得到f[i][j]
的所有结果(1≤j≤size(i))。
为什么连通块强制包含i呢?因为有了根节点,才能保证是连通的呀。
接着做一遍背包:
1 2 3 4 5 6
| for (int i=1;i<=size[x];i++) for (int j=0;j<=size[child];j++) tempmax[i+j]=max(tempmax[i+j],f[x][i]+f[child][j]); size[x]+=size[child]; for (int i=1;i<=size[x];i++) f[x][i]=tempmax[i];
|
上述代码中使用tempmax
数组的目的是在记录答案的同时不改变f
数组中的数据。
遍历完x的所有子节点后,我们把f[x][j]的答案更新到r[j]中。
最小值同理,再做一遍即可。
背包
这是ta出的测试题,不算太难。
使用大神器bitset。开一个比总和大的bitset,初始化bs[0]=1,对于每一个数,将bs∣=bs<<a[i],意为从前可行的每一个和,加上a[i]也是可行的。计算结果时从2sum开始往上找即可。
bzoj权限题,kule。
继续动用大神器。由于是异或,因此同一个和出现两次和没有出现是一样的,出现奇数次相当于出现1次。因此只需保存每一个和出现次数mod2的值。初始化和处理过程和上题相同,最后计算答案时扫描bitset,把为1的索引的值异或起来即得答案。
题意:对于集合A中的某一个数x,若∃B⊂A,x∈B,∑y∈By<k,且∑y∈B∪{x}≥k,则称x是必要的。求集合中不必要的数的个数。
注意到∅⊂A,而对于x≥k,令B=∅,则∑y∈By=0<k,∑y∈B∪{x}=x≥k,知x是必要的。因此,不必要的数必小于k。
我们还可以证明单调性:若a≤b,且a是必要的,那么b也是必要的。
证明是,由于a是必要的,知存在满足上述条件的集合B。
若b∈B,则∑y∈B∪{b}≥∑y∈B∪{a}≥k。
若b∈B,则将B中的b换成a得到集合C,由a≤b知∑y∈Cy≤∑y∈By<k,知C满足题设条件。而∑y∈C∪{b}=∑y∈B∪{a}≥k,证毕。
有了上下界和单调性,我们就可以进行二分了。注意,下面这段代码的真实时间复杂度是O(n2logn),因为bitset的移位操作在复杂度上是O(n)的,只是常数很小而已。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <cstdio> #include <algorithm> #include <bitset> #define MAXN 5005 #define MAXBS 5005 using namespace std; bitset<MAXBS> bs; int a[MAXN]; int main(void){ int n,k; int ll,rr; scanf("%d%d",&n,&k); for (int i=0;i<n;i++){ scanf("%d",a+i); } sort(a,a+n); n=lower_bound(a,a+n,k)-a; ll=-1,rr=n; while (rr-ll>1){ int mid=(ll+rr+1)>>1; int necessary=0; bs.reset(); bs.set(0); for (int i=0;i<n;i++) if (i!=mid) bs|=(bs<<a[i]); bs>>=(k-a[mid]); for (int i=0;i<a[mid];i++){ if (bs.test(i)){ necessary=1; break; } } if (necessary) rr=mid; else ll=mid; } printf("%d\n",ll+1); return 0; }
|
当然,除了二分,我们还有O(n2)的更优的解法。
二分法为什么慢呢?因为对于每一个mid,我们都要计算一遍除了amid以外的元素的组合能达到的数值。下面的方法可以避免重复计算。
我们还需要一个结论。设s=∑i=1kai,如果∃B⊂A,{a1,a2,⋯,ak}∩B=∅,∑y∈By<k,且s+∑y∈B≥k,那么ak是必要的。
证明:假设ak不是必要的,那么根据单调性,a1,a2,⋯,ak都不是必要的。
根据题目条件,将a1,a2,⋯,ak按顺序加入到B中,则必存在1≤j≤k,使加入a1,a2,⋯,ak−1时,和小于k;加入aj时,和大于等于k。因此aj是必要的,与上述假设矛盾。
故假设不成立,ak是必要的。
于是,我们可以根据ak+1,ak+2,⋯,an的组合能达到的范围来确定ak的必要性,这样就避免了bitset的重复计算,时间复杂度降到O(n2)。
数位dp
先计算1≤m<n的整数中出现数字x出现的次数。
依次考虑每一位,考虑个位时令t=1,考虑十位时令t=10,考虑百位时令t=100……
那么在T=10t的一个完整周期内,这一位上数字x共出现了t次。
考虑不完整的那一个周期,如果这一位上的数⌊tn⌋%10>x,那么出现了完整的t次;如果⌊tn⌋%10=x,那么出现了n%t次;如果⌊tn⌋%10<x,那么没有出现。
对于Counting Digits,暴力对每个数求解即可。优化是如果一个x不是解,且函数值为y,那么只比x大一点的数也不会是解,因此x可以增加一个大一点的数。
另外,Counting Digits可以到这里提交。
对于梦中的统计,求出端点相减即可。
“数字计数”是只有一组数据的版本。双倍经验
状态压缩
相当于求哈密顿路。状压。
状态转移的必备技巧(又名FMT,Fans MeeTing Fast Mobius Transform)
已知数组a[0...2k−1]。设b[i]=∑j&i=ja[j](可以认为是A的所有子集的权值之和)。请在O(k2k)的时间内求出b数组的值。
暴力法:对每一个i∈[0...2k−1],枚举j∈[0...2k−1],如果满足条件,则求和。O(4k)。
子集枚举法:对每一个集合A,枚举A的所有子集,进行求和。
如何枚举子集?
1 2 3 4 5 6
| int i; for (int j=i;;j=(j-1)&i){ if (!j) break; }
|
时间复杂度O(3k)。
正解:进行递推。
1 2 3 4 5 6 7 8
| int len=1<<k; for (int i=0;i<k;i++){ int tpow=1<<i; for (int j=0;j<len;j++){ if (j&tpow) f[j]+=f[j^tpow]; } }
|
如何理解上面这段代码呢?
请看拙作第二题
补充:请注意FMT的应用范围!参考洛谷11月月赛 咕咕咕。
Upd: 这个东西又叫做 SoS (Sum over Subsets) dp。
转化:i∣j≤k这个条件很难利用,我们可以求一个数组f[k]=max(Ai+Aj) (i∣j=k),求f[k]的前缀最大值g[k]=max(f[i]) (i≤k),则g[k]即为答案数组。
这么计算同样有难度。我们再进行一次转化:令h[k]=max(Ai+Aj)(i⊆k,j⊆j,i=j),则我们直接用FMT求出k的子集中A的最大值和次大值即可。
期望dp
首先考虑最优策略。假设当前亮着的编号最大的灯是x,则关掉它的最优策略是按下开关x把它关上,同时x的因数的灯的状态会改变。我们接着寻找下一个亮着的灯,继续如此操作。现在我们就能算出,如果一直按照最优策略,需要操作的次数。这样就能拿到k=n的50分了。
现在我们考虑随机情况。设状态k为“此时按照最优策略,还需操作k次”,f[k]表示从状态k转移到状态k−1所需操作次数的期望。
在状态k,如果我们按了正确的开关(即k个正确开关中的一个),那么状态就转移成了k−1;如果我们按了其余的n−k个错误的开关,我们就必须再按一次这个错误的开关,因此状态被转移到了k+1。而状态k+1要转移到状态k−1,首先必须回到状态k。于是得到f[k]=nk×1+nn−k×(1+f[k+1]+f[k])。由此式知f[n]=1,f[k]=k(n−k)f[k+1]+n。可以从f[n]开始,递推得到所有f的值。
因此若设最优策略的步数为t,则答案即为
i=k+1∑tf[i]+i=1∑k1
有分数怎么办?最后不是求得不就是ans×n! %100003的值嘛,因此计算过程中遇到除法就换成乘以逆元就好了。
期望有些很神奇的性质!
期望的线性性质 Ex(k1X+k2Y)=k1Ex(X)+k2Ex(Y) (见维基百科 Expected value)
因此,期望可以乱加(逃)。只要是线性的,我们就方便处理。
考虑一段连续的o,第k个位置和第k+1个位置的得分满足(k+1)2−k2=2k+1是线性的,可以处理。因此我们得出,如果一个位置是o,那么这个位置的得分的期望可以由上一个位置的得分期望计算出。
设f[i]表示到第i步为止得分的期望,g[i]表示在第i步末尾连续o的个数的期望。设u[i]=f[i]−f[i−1]。
现在对第i步的状况进行讨论。
如果第i步是x,那么u[i]=0,g[i]=0。
如果第i步是o,那么根据上面的讨论,u[i]=2g[i−1]+1,g[i]=g[i−1]+1。
如果第i步是?,那么u[i]=(0+2g[i−1]+1)÷2=g[i−1]+0.5,g[i]=g[i−1]+0.5。
于是只需要再使用f[i]=f[i−1]+u[i]计算即可。可以使用滚动数组节省空间。最后的答案即为f[n]。
这是篇好博客,如果看不懂我自己写的就看这里的吧。
暴力法
设f[i]表示从数i到2n−1所需操作次数的期望,则f[2n−1]=0,f[x]=1+∑i=02n−1f[x∣i]×p[i]。于是最暴力的方法就有了:对于每一个x,枚举i⊆x,将p[i]×f[x∣i]进行累加后加上1,再除以f[x]项的系数1−∑i⊆xp[i],即得f[x]。最后输出f[0]。
正解
看题解吧……
利用KMP的next数组。
奇妙的解法
如果只有点权,就很简单了。
加上边权了如何呢?
把边权平分到两个端点上,如果这条边被两个人分别选择,那么差值为0;否则这两半会累加成一个完整的边权。
tql
我们可以发现,无论操作方法如何,从初状态转移到末状态释放的能量是相等的。考虑物理中势能的概念,我们定义一个点(x,y)的单位势能为Es=2x2+y2,一个点的势能为Ep=cnt(star)Es,即星星数目乘以单位势能。定义一个状态的势能为所有点的势能之和。那么从初状态到末状态的能量释放数即为势能差。
答案就是32n。可以使用long double直接输出。
这个答案可以从样例推得。因为每一种语言都是独立的,设语言种数为n,则答案一定是an。代入样例知a=32。
从结束标志0,0,0倒推。
v1.4.18