动态规划从入土到入土
一言(ヒトコト)
\(线性dp\)
略
\(背包dp\)
略
\(状压dp\)
略
\(区间dp\)
一些内容来自某题单
\(\mathscr{概述}\)
依照子区间来定义子问题。 大区间的求解依赖于其内部小区间的求解。 往往用\(F_{l,r,s}\)表示区间\([l,r]\)在状态限制\(s\)下的最优解。按照区间长度从小到大依次求解。 单点构成的区间或空区间无需递归。注意区间边界的加加减减已经初始化,细节比较恶心.不过套路化严重.
\(\mathscr{转移}\)
\(\mathscr{例题}\)
P1880 [NOI1995] 石子合并:环形区间dp模板题,考虑按照区间长度从小到大转移,每次枚举分界点,将分界点左右dp数组值相加,再加上合并产生的分数即为大区间的dp数组值.dp方程:
P1040 加分二叉树:考察了一直学的不好的二叉树三种遍历,还是值得一做的.同时注意转化树上问题为区间问题,但直接在区间上按照显然的方式求最大值最后的结果是否合法并没有搞懂.
P2890 [USACO07OPEN]Cheapest Palindrome G:大水题,但是没秒掉(忘了(1)式了...).题意:字串S长M由N个小写字母构成.欲通过增删字母将其变为回文串,增删特定字母花费不同,求最小花费。唯一的注意点在于若转移时两端的字符相同,直接从\(f_{i+1,j-1}\)转移即可.
P1063 [NOIP2006 提高组] 能量项链:水题,注意l和r细节即可
洛谷上区间dp的题是不是普遍恶评啊
[CF149D Coloring Brackets](CF149D Coloring Brackets - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)):题意:给括号序列中每个括号染色,有两种颜色可选,一对括号只能且必须有一个染色,一个无色,有色的括号只能同色相邻,求方案数.解法:四维dp,状态设计显然,dp时要么预处理+大力转移,要么计搜.细节有亿点多.
\(数位dp\)
略
\(树形dp\)
\(\mathscr{直接}\mathrm{DP}\)
在树上dfs,顺便进行dp.
-
\(Problem\)
P4084[USACO17DEC]Barn Painting G :给一棵树,有些点已经有颜色(0,1,2),求树的相邻点不同色的染色方案数.注意到需要乘法,所以将限定颜色的点的dp数组除了该颜色都置0,该颜色置1,不限定的全部置1,转移较为显然.
P1122 最大子树和:略,比最大子段和还直观....
P3621 [APIO2007] 风铃:并没有太多dp的影子,主要还是一个结论:若一个杆两头需要交换,当且仅当:1)左边都是低深度,右边有高深度2)左边有低深度,右边都是高深度,高低深度互换结论是显然的,但要注意到若左右都有高低深度,就直接无解,原因在于无法将左右子树的一部分交换.所以结论更直观的说法是:对于所有有解的情况,交换两端当且仅当左边存在低深度,右边存在高深度.
P3174 [HAOI2009]毛毛虫:从底向上用最大的毛毛虫更新,每次维护一个最大值和次大值,形成一条链,注意统计链的分支时要特判.
//P3174 #include<iostream> #include<cstdio> #include<vector> using namespace std; const int N=300005; int f[N],ans; vector<int>e[N]; void dfs(int u,int bef) { int maxn=0,submaxn=0; if (e[u].size()==1&&bef!=-1) f[u]=1; for(int i=0; i<(int)e[u].size(); i++) { int v=e[u][i]; if(v==bef) continue; dfs(v,u); f[u]=max(f[u],f[v]);//从最大的子毛毛虫转移 if (f[v]>maxn) submaxn=maxn,maxn=f[v]; else if (f[v]>submaxn) submaxn=f[v]; } //u本身要+1,子节点去掉转移过来的子节点要-1,siz中还有一个指向根节点要-1(根节点不用) if (bef==-1) f[u]+=e[u].size(); else if (e[u].size()>=2) f[u]+=e[u].size()-1; ans=max(ans,max(maxn+submaxn+1,maxn+submaxn+(int)e[u].size()-1)); } int main() { int n,m; scanf("%d%d",&n,&m); for(int i=1,u,v; i<=m; i++) scanf("%d%d",&u,&v),e[u].push_back(v),e[v].push_back(u); dfs(1,-1); printf("%d",ans); return 0; }
\(\mathscr{最小支配集,最小点覆盖与最大独立集}\)
-
\(Note\)
最小支配集:对于图G=(V,E)来说,最小支配集指的是从V中取尽量少的点组成一个集合,使得V中剩余的点都与取出来的点有边相连。也就是说,设V'是图G的一个支配集,则对于图中的任意一个顶点u,要么属于集合V',要么与V'中的顶点相连。在V'中除去任何元素后V'不再是支配集,则支配集V'是极小支配集。称G中所有支配集中顶点个数最少的支配集为最小支配集,最小支配集中的顶点个数称为支配数。
\(树上最小支配集\)
设\(dp_{i,type}\),以点 i 为根的子树都被覆盖的情况时支配集中所包含最少点的个数.
-
\(dp[i][0]\)表示将点 i 选入支配集,那么儿子节点可以任意选取状态.
\[dp[i][0] =\sum_{(u∈son_i )}min(dp[u][0], dp[u][1], dp[u][2])+1 \] -
\(dp[i][1]\)表示不选i且i被子节点覆盖,那么i的子节点要么被选入支配集,要么被它们的子节点覆盖.同时必须有至少一个子节点被选入支配集以覆盖i,考虑先从每个子节点的\(dp_{u,0}\)和\(dp_{u,1}\)中取最优解,并标记是否有子节点选入支配集,存储\(dp[u][0]-dp[u][1]\)的最小值t,若转移结束后发现没有子节点选入独立集,则让\(dp[i][1]\)再加上t.易得:
\[t=\min\{dp[u][0]-dp[u][1]\}\\ dp[i][1] =\left\{\begin{array}{lcl}\sum_{(u∈son_i )}min(dp[u][0], dp[u][1]),t \le 0\\\sum_{(u∈son_i )}min(dp[u][0], dp[u][1])+t,t>0 \end{array} \right. \] -
\(dp[i][2]\)表示不选i且i被父节点覆盖,那么i的子节点不能被父节点覆盖.
\[dp[i][2]=\sum_{(u∈son_i )}min(dp[u][0],dp[u][1]) \]void dfs(int k,int bef){ dp[k][0]=a[k]; long long temp=INF; for (int i=head[k];i;i=e[i].next){ int v=e[i].to; if (v==bef) continue; dfs(v,k); long long t=min(dp[v][0],dp[v][1]); dp[k][0]+=min(t,dp[v][2]); dp[k][1]+=t,dp[k][2]+=t; temp=min(temp,dp[v][0]-dp[v][1]); } if (temp>0) dp[k][1]+=temp; } int main(){ init(); dfs(1,0); cout<<min(dp[1][0],dp[1][1])<<endl; }
最小点覆盖:对于图G=(V,E)来说,最小点覆盖指的是从V中取尽量少的点组成一个集合,使得E中所有的边都与取出来的点相连。也就是说设V'是图G的一个顶点覆盖,则对于图中任意一条边(u,v),要么属于u要么属于集合V'。在V'中除去任何元素后V'不再是顶点覆盖,则V'是极小点覆盖。称G中所有顶点覆盖中顶点个数最小的覆盖为最小点覆盖。
\(树上最小点覆盖\)
void dfs(int k,int bef){ dp[k][0]=a[k]; long long temp=INF; for (int i=head[k];i;i=e[i].next){ int v=e[i].to; if (v==bef) continue; dfs(v,k); long long t=min(dp[v][0],dp[v][1]); dp[k][0]+=min(t,dp[v][2]); dp[k][1]+=t,dp[k][2]+=t; temp=min(temp,dp[v][0]-dp[v][1]); } if (temp>0) dp[k][1]+=temp; } int main(){ init(); dfs(1,0); cout<<min(dp[1][0],dp[1][1])<<endl; }最大独立集:对于图G=(V,E)来说,最大独立集指的是从V中取尽量多的点组成一个集合,使得这些点之间没有边相连。也就是说设V'是图G的一个独立集,则对于图中任意一条边(u,v),u和v不能同时属于集合V',甚至可以u和v都不属于集合V'。在V'中添加任何不属于V'元素后V'不再是独立集,则V'是极大独立集。称G中所有顶点独立集中顶点个数最多的独立集为最大独立集。树,基环树,仙人掌最大独立集
-
-
\(Problem\)
P2458[SDOI2006]保安站岗:树上最小支配集
P2016 战略游戏:树上最小点覆盖
P1352 没有上司的舞会:树上最大独立集
这三个问题也有贪心解法(waiting)
\(\mathscr{树形背包}\)
把背包搬到树上,没有太大变化,主要是分辨背包类型(以及会写背包).
-
\(Problem\)
P1273 有线电视网:理论上是分组背包,
但由于太弱看不出来,写了一个奇怪的逼近\(O(n^3)\)做法(其实是不会证复杂度),实际应该是\(O(n*size_n^2)\),\(size_n\)是子树中叶子节点的个数....,有时间补一个背包(
\(\mathscr{换根}\mathrm{DP},\mathscr{二次扫描}\)
先令一个点为根,求出答案,再每次使根节点偏移到相邻点,快速计算出新答案。
-
\(Problem\)
P2986 [USACO10MAR]Great Cow Gathering G:换根dp模板题.题意:一棵树有点权和边权,求一个点使其他点到这个点的路径长乘以点权之和最小.解法:第一遍dfs求出以1为根节点的答案和每个子树的大小,第二次dfs换根,每次将移远根的和移近根用子树大小快速统计.转移方程:
\[dp_{v}=dp_{u}-size_v*dis_{u,v}+(Sum-size_v)*dis_{u,v} \]
\(\mathscr{基环树与}\mathrm{DP}\)
-
\(Note:基环树\)
-
一棵树+一条返祖边.对于有向图,若每个点入度为1,为外向基环树;若每个点出度为1,为内向基环树.
-
找环
基环树的基本操作.对于无向基环树,用拓扑排序将不在环上的点去掉,剩下的即为环上点.对于有向基环树,在树上dfs,当发现下一个节点已经访问过时,沿着反方向倒回去,经过的点就是环上的点.
//direction void getloop(int k){ vis[k]=++tot; for (int i=head[k];i;i=e[i].next){ int v=e[i].to; if (v==fa[k]) continue; if (vis[v]){ if (vis[v]<vis[k]) continue; loop[++cnt]=k; in_loop[k]=1; while (v!=k){ loop[++cnt]=v; in_loop[v]=1; v=fa[v]; } }else{ fa[v]=k; getloop(v); } } } //undirection void getloop(){ for (int i=1;i<=n;i++) if (in[i]==1) q.push(i); while (!q.empty()){ int t=q.front(); q.pop(); for (int i=head[t];i;i=e[i].next){ int v=e[i].to; if (in[v]>1){ in[v]--; if (in[v]==1) q.push(v); } } } }
-
\(\mathscr{虚树}\)
\(\mathscr{长链剖分}\)
\(优化\)
\(\mathscr{单调队列}\)
-
\(Note1\)
将dp方程中的同一变量放在一起.作为一个单元函数。并满足随着另一个变量变化,该函数取值区间单调变化,用单调队列维护该函数的极值点,从而在均摊O(1)的时间复杂度求出该单元函数在当前区间的极值进行转移。如:(P2627)
对于每次i的不同取值,将\(j=i-K-1\)的情况出队,同时将\(j=i\)的情况入队。
参考P2627,POJ1821
-
\(Problem1\)
(P2627)在n个数字中任意选,不能连续超过k个,求最大值
单调队列里存的应当是用于转移的下标(写成把dp数组值直接存进单调队列WA了好久)
\\P2627
\\这样写不用提前将k个压进队列,也不用给head和tail赋初值,不太懂为什么有的写法要那样.
#include<iostream>
#include<cstdio>
using namespace std;
const int N=100005;
int n,k;
long long a[N],sum[N],dp[N];
long long q[N],head,tail;
int main(){
ios::sync_with_stdio(false);
cin>>n>>k;
for (int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
for (int i=1;i<=n;i++){
while (tail>=head&&q[head]<=i-k-1) head++;
while (tail>=head&&dp[q[tail]-1]-sum[q[tail]]<=dp[i-1]-sum[i]) tail--;
q[++tail]=i;
dp[i]=sum[i]+dp[q[head]-1]-sum[q[head]];
}
cout<<dp[n]<<endl;
}
-
\(Note2\)
仍然维护一个单元函数,但dp变成二维. 参考P2569(细节很多),蓝书P314 Fence
-
\(Problem2\)
(P2569)
写了一下午???,不给\(head\)和\(tail\)赋初值\((head=1,tail=0)\)会爆零,for赋初值也会爆零,没太搞懂为什么.总之对于任意一天都有三种操作:啥也不干,买,卖,对于买卖,从\(i-w-1\)转移就可以(*).原因在于中间\(w\)天不能操作都是从前面一天直接赋值,而由于dp状态并没有保证i-w-1天买卖了,所以i-w-1保存的就是前面天数的最优解法,而之前不买任何股票对于任何一天转移过来都是一样的,所以也没有必要单独考虑.另外注意到如果卖出,单调队列维护的信息在后面,所以倒着来比较简单.
\\和某题解极为相像...照着调到最后变量名都改成一样的了(
\\但是为什么这个题是l=1,r=0啊
\\把上面那个题改成这样就爆零了...
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=2005;
int T,maxp,w,ans=-1,ap,bp,as,bs;
int f[N][N],q[N],l,r;
int main(){
//freopen("1.in","r",stdin);
ios::sync_with_stdio(false);
cin>>T>>maxp>>w;
memset(f,128,sizeof(f));
for (int i=1;i<=T;i++){
cin>>ap>>bp>>as>>bs;
for (int j=0;j<=as;j++)
f[i][j]=-j*ap;
for (int j=0;j<=maxp;j++)
f[i][j]=max(f[i][j],f[i-1][j]);
if (i<=w) continue;
l=1,r=0;//?????
for (int j=0;j<=maxp;j++){
while (l<=r&&q[l]<j-as) l++;
while (l<=r&&(f[i-w-1][q[r]]+q[r]*ap)<=(f[i-w-1][j]+j*ap)) r--;
q[++r]=j;
f[i][j]=max(f[i][j],f[i-w-1][q[l]]+q[l]*ap-j*ap);
}
l=1,r=0;
for (int j=maxp;j>=0;j--){
while (l<=r&&q[l]>j+bs) l++;
while (l<=r&&(f[i-w-1][q[r]]+q[r]*bp)<=(f[i-w-1][j]+j*bp)) r--;
q[++r]=j;
f[i][j]=max(f[i][j],f[i-w-1][q[l]]+q[l]*bp-j*bp);
}
}
for (int i=0;i<=maxp;i++)
ans=max(ans,f[T][i]);
cout<<ans<<endl;
}
-
\(Note3\)
单调队列维护的极值点是一个整体,而不再是一个变量.
-
\(Problem3\)
(CF939F)
一眼看上去就不会做,大致题意是将区间划分为黑白总长度相等的许多区间,但分割点只能在给定的\(k\)个区间里.\((n\leq 10^5,k\leq10^2)\),求最小分割点数. 第一眼以为是扫一下乱搞.(当然是假的)
考虑dp,既然求最小分割点数,首先容易确定dp数组里存放的应是分割点数,其次dp的第一维必然是关于当前状态的右端点,这样第二维只剩下了煎的时间.如果设\(f_{i,j}\)表示前i时间某一面煎了\(j\)时间,如果将"某一面"定为"具体的"一个面,必然在转移时需要知道某状态下朝上/下的面是哪个具体的面,就增加了dp维数,如果将其设为"朝上的面",那么就可以直接通过保证\(j\)来保证是否是同一面,因为若\(j\)不变,就说明之前的状态和当前状态朝上的面是同一面,或者是煎的时间相同的不同面(但这是等效的,即上下面煎的时间一样时具体哪个面朝上是无所谓的),而只要从不同的\(j\)转移过来,就要么是之前相同的面由于翻转被煎改变了\(j\),要么是不同的面变为朝上了,还可以发现,对于一个确定的i和j,朝下的面煎的时间也是确定的,所以这样的dp表示状态是可行的(牢记\(j_{back}=i-j_{head}\)).
考虑转移,在i时间只有翻转和不翻转两种选择,不翻转可以直接从\(f_{i-1,j}\)继承,而翻转则需要枚举翻转点,这样是\((n^2)\)的,显然没有用到题目中的\(k\leq100\),虽然保证转移一定从给定区间,但实际上和全部依次转移是一样的.
转移方程
\[f_{i,j}=min\{f_{i-1,j},d_{i-1,i-j}+1\} \]\(f_{2n,n}\)即为结果.
考虑优化,一个可分割的区间,必然最多分割两次来保证时间正确的前提下出入区间的状态(也就是说保证出去的面是正确的),显然考虑在区间之间转移.设\(f_{i,j}\)表示到第i个区间末端朝上的面总共被烤了j时间,那么状态数显然是\(nk\)的,当本区间不翻面,就直接从\(f_{i-1,j}\)转移,因为不翻面朝上的面不会被烤,相比设朝下的面为状态简化了转移.当本区间翻一次面,朝上的面就改变为不同的面,设第i个区间的末尾是r,又因为到本区间末朝上的面被煎了j时间,那么到本区间末朝下的面就煎了\(r-j\)时间,若本区间朝下的面煎了k时间,则到上一区间末朝上的面被煎了\(r-j-k\)时间.当本区间翻两次面,朝上的面与上一区间相同,若本区间里朝上的面被煎了k时间,则到上一区间末朝上的面就煎了j-k时间,得到转移方程:
\[f_{i,j}=min\{f_{i-1,j},f_{i-1,r_i-j-k}+1,f_{i-1,j-k}+2\}$$ $(0 \le k \le len_i=r_i-l_i) \]\(f_{k,n}\)即为结果,因为有\(r_i-j-len_i \le r_i-j-k \le r_i-j-0,即l_i-j \le r_i-j-k \le r_i-j\)和\(l_i+j-r_i \le j-k \le j,j \le min(n,r_i)\),把枚举\(r_i-j-k\)等用单调队列做掉即可.(具体写的时候一直在迷惑....)
#include<iostream> #include<cstdio> using namespace std; const int K=105,N=100005,INF=0x7ffffff; int n,k; int l,r,head,tail; int f[K][N],q[N]; int main(){ ios::sync_with_stdio(false); cin>>n>>k; for (int i=0;i<=k;i++) for (int j=0;j<=n;j++) f[i][j]=INF; f[0][0]=0; for (int i=1;i<=k;i++){ cin>>l>>r; for (int j=0;j<=n;j++) f[i][j]=min(f[i][j],f[i-1][j]); head=1,tail=0; //最迷惑的地方在于循环的时候t代表的是(j+k),而边界式子里把t当成j带进去竟然是对的,而且我也不知道不这么写能怎么写,下同 for (int t=r;t>=0;t--){ while (head<=tail&&q[head]<l-t) head++; while (head<=tail&&f[i-1][q[tail]]>f[i-1][r-t]) tail--; q[++tail]=r-t; f[i][t]=min(f[i][t],f[i-1][q[head]]+1); } head=1,tail=0; for (int t=0;t<=min(n,r);t++){ while (head<=tail&&q[head]<l-r+t) head++; while (head<=tail&&f[i-1][q[tail]]>f[i-1][t]) tail--; q[++tail]=t; f[i][t]=min(f[i][t],f[i-1][q[head]]+2); } } if (f[k][n]!=INF) cout<<"Full"<<endl<<f[k][n]<<endl; else cout<<"Hungry"<<endl; }
杂题选做
CF148E Porcelain:
- 好题,
完美诠释dp圣经.题意:有n个由正数组成的序列,取m次,每次只能从两端取,使总和最大.直接两维dp难以着手,考虑一种想法: - 发现对于每一个序列,取固定次数能获得最大价值是固定的,显然可以先预处理出结果,然后问题转化为:
- 1)有一个序列只能从两端取,求取k次的最大价值;容易发现LRLR和LLRR是等效的,即我们不关心取的顺序,只关心最终在左右分别取了多少个,先求出前缀和,然后枚举即可.
- 2)有n组物品,每组物品只能选一个,物品有价值和体积,求在给定体积下的最大价值.这里的“一组物品”指的是将同一序列中取法的取的次数作为体积,取得的价值作为价值形成的一个组,显然这个组中物品只能选一个.运用分组背包模板即可解决.
- 解决此题的难点在于需要发现在序列间选择和在序列内选择是独立的,从而防止无用的大量状态转移.
- 然而WA飞了....因为每个序列不一样长,所以使用偷懒的后缀和就没了....以后还是至少清一下len+1位置的数组...
//VSCode的格式化怎么这么难看...要不是有某二次元主题早就换了
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 105, M = 10005;
int n, m;
int k[N], a[N], sum1[N], sum2[N], sub[N][N], dp[N][M];
int main()
{
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
memset(sum1, 0, sizeof(sum1));
memset(sum2, 0, sizeof(sum2));
cin >> k[i];
for (int j = 1; j <= k[i]; j++)
cin >> a[j];
for (int j = 1; j <= k[i]; j++)
sum1[j] = sum1[j - 1] + a[j];
for (int j = k[i]; j >= 1; j--)
sum2[j] = sum2[j + 1] + a[j];
for (int t = 1; t <= k[i]; t++)
for (int l = 0; l <= t; l++)
sub[i][t] = max(sub[i][t], sum1[l] + sum2[k[i] - (t - l) + 1]);
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int t = 0; t <= min(j, k[i]); t++) //j要大于等于t
dp[i][j] = max(dp[i][j], dp[i - 1][j - t] + sub[i][t]);
cout << dp[n][m] << endl;
}
CF296B Yaroslav and Two Strings](https://www.luogu.com.cn/problem/CF296B):
阴间题,大力转移要转一页纸(不愧是CF).

一遍就A了(主要在于样例真是强),把dp数组小的维放前面就比rk2快一倍(再写个取模优化是不是要快114514倍
题意:给定两个由数字和问号组成的等长字符串\(A,B\),定义性质\(P:\exist 1 \le i,j \le len\),满足\(A_i < B_i \and A_j > B_j\),求将问号替换成数字使\(A,B\)满足性质\(P\)的方案数.
考虑序列dp,观察到性质是两方面的,一方面是令A中有数大于B,另一方面则是小于,这两方面显然不能在一次转移中完成,故在状态设计中加入两维,分别记录是否已满足这两方面性质,再加一维表示序列的前i个,即用\(dp_{0/1,0/1,i}\)存储方案数.
考虑转移,由于不可能一次满足两个性质,显然满足一个性质的要从不满足任何性质和满足同一性质转移过来,而满足两个性质的要从两种满足一个性质和已经满足两个性质的转移过来,不满足任何性质的只能从之前不满足任何性质的转移过来,同时注意根据问号数量讨论:若都不是问号,根据数字大小关系直接转移;若有一个问号,考虑在满足给定性质时问号的方案数,显然种类并不多,可以提前算出.易得转移方程:
//写转移时始终想着一次最多只能满足一方面性质就会好写很多
#include<iostream>
#include<cstdio>
using namespace std;
const int N=100005,Mod=1000000007;
int n,a[N],b[N];
long long dp[2][2][N];
string s1,s2;
int main() {
ios::sync_with_stdio(false);
cin>>n>>s1>>s2;
s1=' '+s1,s2=' '+s2;//让下标从1开始
for (int i=1; i<=n; i++) {
if (s1[i]=='?')a[i]=-1;//问号标为-1
else a[i]=s1[i]-'0';
if (s2[i]=='?')b[i]=-1;
else b[i]=s2[i]-'0';
}
dp[0][0][0]=1;
for (int i=1; i<=n; i++) {
if (a[i]!=-1&&b[i]!=-1) {
if (a[i]==b[i]) {//直接转移
dp[0][0][i]=dp[0][0][i-1]%Mod;
dp[0][1][i]=dp[0][1][i-1]%Mod;
dp[1][0][i]=dp[1][0][i-1]%Mod;
dp[1][1][i]=dp[1][1][i-1]%Mod;
}
if (a[i]>b[i]) {//只能满足一方面性质
dp[1][0][i]=dp[0][0][i-1]%Mod+dp[1][0][i-1]%Mod;
dp[1][1][i]=dp[0][1][i-1]%Mod+dp[1][1][i-1]%Mod;
}
if (a[i]<b[i]) {//另一方面
dp[0][1][i]=dp[0][0][i-1]%Mod+dp[0][1][i-1]%Mod;
dp[1][1][i]=dp[1][0][i-1]%Mod+dp[1][1][i-1]%Mod;
}
continue;
}
if (a[i]!=-1&&b[i]==-1) {//单问号
dp[0][0][i]=dp[0][0][i-1]%Mod;
dp[0][1][i]=dp[0][1][i-1]*(10-a[i])%Mod+dp[0][0][i-1]*(9-a[i])%Mod;
dp[1][0][i]=dp[1][0][i-1]*(a[i]+1)%Mod+dp[0][0][i-1]*a[i]%Mod;
dp[1][1][i]=dp[1][0][i-1]*(9-a[i])%Mod+dp[0][1][i-1]*a[i]%Mod+dp[1][1][i-1]*10%Mod;
continue;
}
if (a[i]==-1&&b[i]!=-1) {//单问号
dp[0][0][i]=dp[0][0][i-1]%Mod;
dp[0][1][i]=dp[0][1][i-1]*(b[i]+1)%Mod+dp[0][0][i-1]*b[i]%Mod;
dp[1][0][i]=dp[1][0][i-1]*(10-b[i])%Mod+dp[0][0][i-1]*(9-b[i])%Mod;
dp[1][1][i]=dp[1][0][i-1]*b[i]%Mod+dp[0][1][i-1]*(9-b[i])%Mod+dp[1][1][i-1]*10%Mod;
continue;
}
if (a[i]==-1&&b[i]==-1) {//双问号
dp[0][0][i]=dp[0][0][i-1]*10%Mod;
dp[0][1][i]=dp[0][0][i-1]*45%Mod+dp[0][1][i-1]*55%Mod;
dp[1][0][i]=dp[0][0][i-1]*45%Mod+dp[1][0][i-1]*55%Mod;
dp[1][1][i]=dp[0][1][i-1]*45%Mod+dp[1][0][i-1]*45%Mod+dp[1][1][i-1]*100%Mod;
continue;
}
cout<<"Fail"<<endl;
}
cout<<dp[1][1][n]%Mod<<endl;
}
@uptracer 写的更为巧妙:大大减小了转移的难度。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=100010,mod=1e9+7;
int n;
char s[N],w[N];
ll solve(char *s,char *w)// s[i]<=w[i]
{
ll ans=1;
for(int i=1;i<=n;i++)
{
if(s[i]=='?'&&w[i]=='?') ans=ans*55%mod;
else if(s[i]=='?'&&w[i]!='?') ans=ans*(w[i]-'0'+1)%mod;
else if(s[i]!='?'&&w[i]=='?') ans=ans*(10+'0'-s[i])%mod;
else if(s[i]>w[i]) return 0;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin>>n;
cin>>s+1>>w+1;
ll ans=1;
for(int i=1;i<=n;i++)
{
if(s[i]=='?') ans=ans*10%mod;
if(w[i]=='?') ans=ans*10%mod;
}
ans=((ans-solve(s,w)-solve(w,s))%mod+mod)%mod;
ll ret=1;
for(int i=1;i<=n;i++)
{
if(s[i]=='?'&&w[i]=='?') ret=ret*10%mod;
else if(s[i]!=w[i]&&(s[i]!='?'&&w[i]!='?')) ret=0;
}
//cout<<solve(s,w)<<'\n'<<solve(w,s)<<'\n'<<ret<<'\n';
ans=(ans+ret)%mod;
cout<<ans<<'\n';
return 0;
}
CF847E Packmen:
-
奇怪的东西
- WA了两小时
- 比rk2快一倍,但代码长4倍
请思考本题用dp怎么做
-
题意
今有一区间,人,物具陈其上,间或亦有空.其人可左右徙于上.求让人的移动轨迹覆盖所有物品的情况下,人移动的路程的最大值的最小值. -
题解
根据题意,显然看出是二分.再考虑一个人的走法:
-
向左走,不回头
-
向右走,不回头
-
向左再向右,左边重复走
-
向右再向左,右边重复走
-
若我们从左到右把人扫一遍,必然要保证先把左边的没走到的物品走到,同时在还有步数的情况下向右走.
这样会WA on #7 ,原因在于先向右走再向左走到第一个没走到的物品可能更优,因为重复的部分可能更少.
考虑分类讨论:用一个物品指针指向还没走到的最靠左的物品
-
步数不够到达最左边的没走到的物品\(\to\)直接
return false -
步数只能到最左边的物品,无法向右走\(\to\)把物品指针向右更新到人右边
-
步数很充足\(\to\)在3,4两种走法中取最优解
-
指针在人右边\(\to\)把物品指针尽可能向右更新
结束后判断指针是否大于\(Cnt_{package}\)即可
这样就又WA了,原因在于二分的右边界应当是\(\frac{3n}{2}\),即只有一个人在正中间,先向某方向然后再折返的答案,程序里为了简单写成了\(2n\).
最终复杂度:\(O(Cnt_{people}\log2n)\),明显快于\(O(n\log2n)\).
-
代码
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 100005;
int n, L, R, cnt1, cnt2;
int peo[N], pack[N];
string s;
bool check(int k)
{
int await_pick = 1;
for (int i = 1; i <= cnt1; i++)
{
int start_pos = await_pick;
if (await_pick > cnt2)
return true;
if (pack[start_pos] < peo[i])
{
if (pack[start_pos] < peo[i] - k)//第一种情况
return false;
if (k <= peo[i] - pack[start_pos] + 1)//第二种情况
{
while (pack[await_pick + 1] <= peo[i] && await_pick <= cnt2 && pack[await_pick + 1] <= peo[i + 1])
await_pick++;
if (pack[await_pick] <= peo[i] && pack[await_pick] <= peo[i + 1])
await_pick++;
}
else
{
if (k > (2 * (peo[i] - pack[start_pos])))
{//第三种情况,先向左走
int temp = k - (2 * (peo[i] - pack[start_pos]));
while (pack[await_pick + 1] <= peo[i] + temp && await_pick <= cnt2 && pack[await_pick + 1] <= peo[i + 1])
await_pick++;
if (pack[await_pick] <= peo[i] + temp && pack[await_pick] <= peo[i + 1])
await_pick++;
}
if (peo[i] + (k + pack[start_pos] - peo[i]) / 2 >= pack[await_pick])
{//第三种情况,先向右走
int temp = (k + pack[start_pos] - peo[i]) / 2;
while (pack[await_pick + 1] <= peo[i] + temp && await_pick <= cnt2 && pack[await_pick + 1] <= peo[i + 1])
await_pick++;
if (pack[await_pick] <= peo[i] + temp && pack[await_pick] <= peo[i + 1])
await_pick++;
}
}
}
else
{//第四种情况
while (pack[await_pick + 1] <= peo[i] + k && await_pick <= cnt2 && pack[await_pick + 1] <= peo[i + 1])
await_pick++;
if (pack[await_pick] <= peo[i] + k && await_pick <= cnt2 && pack[await_pick] <= peo[i + 1])
await_pick++;
}
}
if (await_pick > cnt2)
return true;
else
return false;
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> s;
for (int i = 0; i < s.length(); i++)
{
if (s[i] == 'P')
peo[++cnt1] = i + 1;
if (s[i] == '*')
pack[++cnt2] = i + 1;
}
peo[cnt1 + 1] = 0x7fffffff;
pack[cnt2 + 1] = 0x7fffffff;
L = 1, R = 2 * n;//注意二分边界
while (L <= R)
{
int mid = (L + R) / 2;
if (check(mid))
R = mid - 1;
else
L = mid + 1;
}
cout << L;
}
CF533B Work Group
给定一个带点权的以1为根的树,求一个最大权点集,使其中每个点的子树中有偶数个点被选中.
考虑树形dp,设\(dp_{i,0/1,0/1}\)表示第i个点不被选/被选的情况下,包括i在内的子树选偶数/奇数个点的最大权值.
考虑转移,由于偶数加奇数是奇数,奇数加偶数是奇数,偶数加偶数是偶数,奇数加奇数是偶数.所以维护两个\(sum\),分别保存在i的子树中选奇数/偶数的最大收益.显然奇数的\(sum_1\)可以由偶数的\(sum_2\)选上一个新的奇数的值转移过来,反之亦然.由此可得\(sum\)的转移方程:
但是这样会WA,原因在于若\(sum_1\)没有存放东西,则选了0个点,不符合\(sum_1\)的定义,此时不能用\(sum_1\)进行转移.当\(sum_1=0\)时,转移方程为:
又WA了,注意将\(sum_1,sum_2\)的值先取出来,防止改变了其中一个再将其用于转移.
这样还是WA了,原因在于叶子结点dp数组赋值为0,对于dpk10和dpk01两种不合法状态应当赋值为-INF.
这样又WA了,显然\(dp_{i,1,1}\)仅在叶子结点合法,要特判.
又WA了....答案应当是\(max(max(dp[1][1][1], dp[1][0][0]), dp[1][0][1])\),同样是保证合法性.
#include <iostream>
#include <cstdio>
using namespace std;
#define int long long
const int N = 500005, M = 1000005, INF = 0x7fffffffffffff;
int n, cnt;
int a[N], head[N], fa[N], siz[N], dp[N][2][2];
struct edge
{
int to, next;
} e[M];
void add(int u, int v)
{
e[++cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
void dfs(int k)
{
if (siz[k] == 0)
{
dp[k][1][1] = a[k];
dp[k][1][0] = -INF;
dp[k][0][1] = -INF;
dp[k][0][0] = 0;
return;
}
int sum1 = 0, sum2 = 0; //sum1:在k的子树中选奇数个点的最大收益,称奇和;sum2:在k的子树中选偶数个点的最大收益,称偶和.
for (int i = head[k]; i; i = e[i].next)
{
int v = e[i].to;
if (v == fa[k])
continue;
dfs(v);
int temp1 = sum1, temp2 = sum2;
if (temp1 != 0) //等于0的奇和不是奇和,因为选了0个点,是偶和,不能参与转移
{
sum1 = max(temp2 + max(dp[v][1][1], dp[v][0][1]), temp1 + max(dp[v][1][0], dp[v][0][0])); //奇=偶和+奇数,奇和+偶数
sum2 = max(temp1 + max(dp[v][1][1], dp[v][0][1]), temp2 + max(dp[v][1][0], dp[v][0][0])); //偶=奇和+奇数,偶和+偶数
}
else
{
sum1 = temp2 + max(dp[v][1][1], dp[v][0][1]); //奇=偶和+奇数
sum2 = temp2 + max(dp[v][1][0], dp[v][0][0]); //偶=偶和+偶数
}
}
//不能dp[k][1][0] = sum1 + a[k];//选k号点并且其子树选奇数个,是不合法状态.
dp[k][1][1] = sum2 + a[k];
dp[k][0][1] = sum1;
dp[k][0][0] = sum2;
}
signed main()
{
ios::sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> fa[i] >> a[i];
if (i != 1)
add(i, fa[i]), add(fa[i], i), siz[fa[i]]++;
}
dfs(1);
cout << max(max(dp[1][1][1], dp[1][0][0]), dp[1][0][1]) << endl; //即使根节点下选奇数个点,只要根节点不选,也是合法的.
}
CF1201D Treasure Hunting
一个\(n*m\)的网格上有许多物品,一开始在(1,1),每次可以向左右走,仅在给定的几个列可以向上走,求取完所有物品的最短路程. \(n,m \leq 2*10^5\)
显然不适合最短路,原因在于每次到达一行必定要走完这一行的所有物品,而穿过物品不回头从另一端离开显然不一定是最优的.
考虑dp,容易发现,对于每层,只需要考虑取完该层物品之后从哪里去往下一层,而取完物品时一定位于物品块的两端,于是从每层物品块两端分别向下一层物品块的两端转移,对于每个转移,考虑依次尝试离四个端点的最近的两个通道,显然没有比这四个通道更优的方法(其他都是冤枉路),具体计算路程时还要考虑是否顺路完成了物品块的覆盖,以及是否需要走回头路.
细节较多,注意分类讨论:四端转移,每对有四种通道,每条路有四种通道与两端的相对位置,每种相对位置有左右端与左右端是否超过通道需要走回头路四种情况,共有\(4*4*4*4=64\)种情况,依次讨论即可.
注意几个坑点:
- 端点可能就在通道上,.不用找左右端直接尝试转移即可.
- 从某层往上可能都没有物品,要把它们删掉.
- 某两层间可能有多层没有物品,用指针记录上一个有物品的层即可.
转移方程:
#include <iostream>
#include <cstdio>
#define int long long
using namespace std;
const int N = 200005, INF = 0x7ffffff;
int n, m, k, q;
bool treasure_in_line[N];
int left_of_line[N], right_of_line[N], safecolumn[N], closest_left_safe_column[N], closest_right_safe_column[N], len[N];
int dp[N][2];
namespace solve
{
void init()
{
ios::sync_with_stdio(false);
cin >> n >> m >> k >> q;
for (int i = 1; i <= n; i++)
left_of_line[i] = m + 1, right_of_line[i] = -1;
for (int i = 1; i <= k; i++)
{
int x, y;
cin >> x >> y;
treasure_in_line[x] = 1;
left_of_line[x] = min(left_of_line[x], y);
right_of_line[x] = max(right_of_line[x], y);
}
for (int i = 1; i <= n; i++)
len[i] = right_of_line[i] - left_of_line[i];
for (int i = 1; i <= q; i++)
{
int temp;
cin >> temp;
safecolumn[temp] = 1;
}
for (int i = 1; i <= m; i++)
{
if (!safecolumn[i])
continue;
closest_left_safe_column[i] = i;
closest_right_safe_column[i] = i;
int templ = i - 1, tempr = i + 1;
while (templ >= 1 && !safecolumn[templ])
{
closest_right_safe_column[templ] = i;
templ--;
}
while (tempr <= m && !safecolumn[tempr])
{
closest_left_safe_column[tempr] = i;
tempr++;
}
}
int cnt = n;
for (cnt; cnt >= 1; cnt--)
if (treasure_in_line[cnt])
break;
if (cnt < n)
n = cnt;
}
int real_distant(int line1, int line2, int column1, int column2, int path, int lr1, int lr2)
{
int sum;
if (path <= column1 && path <= column2)
{
sum = column1 - path + column2 - path + 1;
if (lr2 == 0)
sum += 2 * len[line2];
else
{
if (left_of_line[line2] < path)
sum += 2 * (path - left_of_line[line2]);
}
}
if (path <= column1 && path >= column2)
{
sum = column1 - column2 + 1;
if (lr2 == 1)
sum += 2 * len[line2];
else
{
if (right_of_line[line2] > path)
sum += 2 * (right_of_line[line2] - path);
}
}
if (path >= column1 && path <= column2)
{
sum = column2 - column1 + 1;
if (lr2 == 0)
sum += 2 * len[line2];
else
{
if (left_of_line[line2] < path)
sum += 2 * (path - left_of_line[line2]);
}
}
if (path >= column1 && path >= column2)
{
sum = path - column1 + path - column2 + 1;
if (lr2 == 1)
sum += 2 * len[line2];
else
{
if (right_of_line[line2] > path)
sum += 2 * (right_of_line[line2] - path);
}
}
return sum;
}
int get_dis(int low_line, int high_line, int low_line_column, int high_line_column, int position_low, int position_high)
{
int ans = INF;
if (closest_left_safe_column[low_line_column])
ans = min(ans, real_distant(low_line, high_line, low_line_column, high_line_column, closest_left_safe_column[low_line_column], position_low, position_high));
if (closest_right_safe_column[low_line_column])
ans = min(ans, real_distant(low_line, high_line, low_line_column, high_line_column, closest_right_safe_column[low_line_column], position_low, position_high));
if (closest_left_safe_column[high_line_column])
ans = min(ans, real_distant(low_line, high_line, low_line_column, high_line_column, closest_left_safe_column[high_line_column], position_low, position_high));
if (closest_right_safe_column[high_line_column])
ans = min(ans, real_distant(low_line, high_line, low_line_column, high_line_column, closest_right_safe_column[high_line_column], position_low, position_high));
return ans;
}
}
using namespace solve;
int head=1;
signed main()
{
init();
if (!treasure_in_line[1])
{
dp[1][0] = 0, dp[1][1] = 0;
left_of_line[1] = 1, right_of_line[1] = 1;
len[1] = 0;
}
else
dp[1][0] = left_of_line[1] - 1 + 2 * len[1], dp[1][1] = right_of_line[1] - 1;
for (int i = 2; i <= n; i++)
if (treasure_in_line[i])
{
dp[i][0] = i - head - 1 + min(dp[head][0] + get_dis(head, i, left_of_line[head], left_of_line[i], 0, 0), dp[head][1] + get_dis(head, i, right_of_line[head], left_of_line[i], 1, 0));
dp[i][1] = i - head - 1 + min(dp[head][0] + get_dis(head, i, left_of_line[head], right_of_line[i], 0, 1), dp[head][1] + get_dis(head, i, right_of_line[head], right_of_line[i], 1, 1));
head = i;
}
cout << min(dp[n][0], dp[n][1]);
}
CF730J Bottles
背包变形.有n个水瓶,里面装有ai单位水,容积是bi,(1)问装满所有水用的最少瓶子数.(2)问在瓶子数最小条件下,需要移动水的最小值.
第一问贪心显然成立,考虑排序后依次选入,直到容积大于所需,此时瓶子数即为所求,同时将此时容积记为tot_v,将所有已有水体积之和记为tot_w.由于需要保证瓶子数最小,在dp数组里开一维记录已选的个数,由于又要保证容积足够,再开一维统计已选容积,第一维显然是表示已经考虑前i个瓶子.存储最大不移动的水体积,最后用tot_w减去即可.
注意合法容积是从tot_w到tot_v.(不懂为什么不能一直到所有瓶子容积之和,感觉不会影响答案...),另外已选的个数要从0开始.
CF1158C Permutation recovery
对于一个排列\(a1...an\), \(next_i\)表示排列中i后面第一个比\(ai\)大的位置.现给定一个排列的一部分\(next_i\)数组,试据此构造一个排列.
若\(next_i=n+1\)表示i后面没有比它更大的数,\(next_i=-1\)表示不确定.
本文来自博客园,作者:Kinuhata,转载请注明原文链接:https://www.cnblogs.com/KinuhataSaiai/p/15511467.html

浙公网安备 33010602011771号