动态规划题单1
可恶的动态规划,每次考试基本都写不出来,于是特意整理个动态规划提单
1.CF1620F Bipartite Array
题意等价于:要把这些点分成两部分,每一部分之间都没有边相连,等价于把这个序列中分成两个上升子序列。
在DP时肯定要记录两个序列的末尾,但发现其中一个序列的末尾肯定是 \(a[i]\) 或者 \(-a[i]\) , 因此只需记录另外一个的。
\(f[i][0/1][j]\) 表示把 \(a[i]\) 不取反/取反 作为一个序列的末尾,另一个序列的末尾是 \(j\) 是否可行
由于这个状态值仅仅是可不可行,第三维又很大,于是可以考虑把第三维记录状态:
\(f[i][0/1]\) 表示 把 \(a[i]\) 不取反/取反 作为一个序列的末尾,另一个序列的末尾的最小值(显然另一个序列末尾越小越优)。
在转移时看下一个数放在哪个序列末尾,并记录一下方案即可。
写的可能有点麻烦
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,inf=0x3f3f3f3f;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T,n,a[N],f[N][2],ans[N][2];
bool check(int i,int op){
if(f[i][op]==inf) return false;
if(i==1) return true;
return check(i-1,ans[i][op]);
}
void print(int i,int op){
if(i!=1) print(i-1,ans[i][op]);
if(op==0) printf("%d ",a[i]);
else printf("%d ",-a[i]);
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
T=read();
while(T--){
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=n;i++) f[i][0]=f[i][1]=inf;
f[1][0]=f[1][1]=-inf;
for(int i=2;i<=n;i++){
//转移f[i][0]
if(a[i]>a[i-1]&&f[i-1][0]!=inf){
if(f[i-1][0]<f[i][0]){
f[i][0]=min(f[i][0],f[i-1][0]);
ans[i][0]=0;
}
}
if(a[i]>f[i-1][0]&&f[i-1][0]!=inf){
if(a[i-1]<f[i][0]){
f[i][0]=min(f[i][0],a[i-1]);
ans[i][0]=0;
}
}
if(a[i]>-a[i-1]&&f[i-1][1]!=inf){
if(f[i-1][1]<f[i][0]){
f[i][0]=min(f[i][0],f[i-1][1]);
ans[i][0]=1;
}
}
if(a[i]>f[i-1][1]&&f[i-1][1]!=inf){
if(-a[i-1]<f[i][0]){
f[i][0]=min(f[i][0],-a[i-1]);
ans[i][0]=1;
}
}
//转移f[i][1]
if(-a[i]>a[i-1]&&f[i-1][0]!=inf){
if(f[i-1][0]<f[i][1]){
f[i][1]=min(f[i][1],f[i-1][0]);
ans[i][1]=0;
}
}
if(-a[i]>f[i-1][0]&&f[i-1][0]!=inf){
if(a[i-1]<f[i][1]){
f[i][1]=min(f[i][1],a[i-1]);
ans[i][1]=0;
}
}
if(-a[i]>-a[i-1]&&f[i-1][1]!=inf){
if(f[i-1][1]<f[i][1]){
f[i][1]=min(f[i][1],f[i-1][1]);
ans[i][1]=1;
}
}
if(-a[i]>f[i-1][1]&&f[i-1][1]!=inf){
if(-a[i-1]<f[i][1]){
f[i][1]=min(f[i][1],-a[i-1]);
ans[i][1]=1;
}
}
}
if(f[n][0]!=inf||f[n][1]!=inf){
printf("YES\n");
if(check(n,1)) print(n,1);
else print(n,0);
puts("");
}
else puts("NO");
}
return 0;
}
2.CF1616H Keep XOR Low
看到位运算,直接就放到 Trie 里面。
一个很自然的想法:设 \(f[i]\) 表示第 \(i\) 棵子数内的方案数
如果当前考虑到了第 \(j\) 位:
- 如果 \(x\) 的第 \(j\) 位为 \(0\),那显然左右子树不能都选,直接 \(f[i]=f[ls]+f[rs]\)。
- 如果 \(x\) 的第 \(j\) 位为 \(1\),这时候就出问题了,因为虽然每棵子树内部可以随便选,但是同时选两棵子树的情况很难转移。
于是大胆一点,设 \(f[u][v]\) 表示在 \(u\) 子树和 \(v\) 子树分别选几个数(可以为空)使他们两两之间满足条件的方案数( \(u\) 可以等于 \(v\))
注意:对 \(u\) 或 \(v\) 自身子树里选数没有限制,即只需满足跨子树的限制,可以这样设计是因为当 \(u \ne v\) 的时候意味着 \(u\) 或 \(v\) 这棵子树内的点异或起来的结果在更高的位上已经比 \(x\) 小了
如果当前考虑到了第 \(j\) 位:
-
如果 \(x\) 的第 \(j\) 位为 \(1\):
同时选 \(u\) 的左儿子和 \(v\) 的左儿子显然是一定满足的,同时选右儿子同理
于是只需要把选 \(u\) 的左儿子和 \(v\) 的右儿子 和 选 \(u\) 的右儿子和 \(v\) 的左儿子 的方案数乘起来
是乘法原理,因为这两步之间是没有限制的 -
如果 \(x\) 的第 \(j\) 位为 \(0\):
此时只能同时选 \(u\) 的左儿子和 \(v\) 的左儿子或同时选右儿子
那就是加法原理,因为这两步如果同时发生会出现 既选了 \(u\) 的左儿子又选了 \(v\) 的右儿子的情况
但注意这里还有一种额外的情况:
因为我们 \(f\) 数组的定义只考虑跨界的异或情况,所以我们还可以只选 \(u\) 的点或只选 \(v\) 的点 ,要额外加上
每个点只会被遍历一遍,故时间复杂度是对的
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=150000+5,M=6e6+5,mod=998244353;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,x;
int tot=1,ch[M][3],Size[M],mi[M];
void insert(int x){
int p=1;
for(int i=30;i>=0;i--){
int c=(x>>i)&1;
Size[p]++;
if(!ch[p][c]) ch[p][c]=++tot;
p=ch[p][c];
}
Size[p]++;
}
int dfs(int u,int v,int d){ //这个计算出的结果可以有空集
if(!u) return mi[Size[v]];
if(!v) return mi[Size[u]];
if(u==v){
if(d==-1) return mi[Size[u]]; //到达叶子
int ls=ch[u][0],rs=ch[u][1];
if(x>>d&1) return dfs(ls,rs,d-1);
else return (dfs(ls,ls,d-1)+dfs(rs,rs,d-1)-1ll+mod)%mod; //空集会被算两次所以-1
}
if(d==-1) return mi[Size[u]+Size[v]];
int ls1=ch[u][0],ls2=ch[v][0],rs1=ch[u][1],rs2=ch[v][1];
if(x>>d&1) return dfs(ls1,rs2,d-1)*dfs(rs1,ls2,d-1)%mod;
else{
int ans=(dfs(ls1,ls2,d-1)+dfs(rs1,rs2,d-1)-1ll+mod)%mod;
(ans += ( mi[Size[ls1]] - 1ll + mod ) * ( mi[Size[rs1]] -1ll + mod ) )%=mod;
(ans += ( mi[Size[ls2]] - 1ll + mod ) * ( mi[Size[rs2]] -1ll + mod ) )%=mod;
//要减掉左右子树中有一个不选的情况,因为(dfs(ls1,ls2,d-1)+dfs(rs1,rs2,d-1)-1ll+mod)算过了
return ans;
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),x=read();
for(int i=1;i<=n;i++) insert(read());
mi[0]=1;
for(int i=1;i<=n;i++) (mi[i]=mi[i-1]*2ll)%=mod;
printf("%lld\n",(dfs(1,1,30)-1ll+mod)%mod); //不能是空集
return 0;
}
3.CF1775F Laboratory on Pluto
最小周长的图形一定是个凸的图形而不是凹的,否则一定不优。
对于一个凸的图形,他的周长可以通过平移转化成能够包含他的最小矩形的周长,比如:
- 第一问:
假设最小矩形边长为 \(a\) , \(b\) ,则需满足 \(a \times b \ge n\) (因为要往里面填 \(n\) 个方块),在此基础上使 \(a+b\) 尽可能小。
假设我们已经确定了 \(a\times b\)的值,那显然 \(a\) , \(b\) 越接近,\(a+b\)越小(小学数学)
具体证明就是\((a+b)^2=(a-b)^2+4ab\)....
那肯定 \(a\) , \(b\) 都取 \(\sqrt{n}\) 最优,当然不一定是整数,自己随便凑一凑就可以了.
其实假设 \(a<b\) , 则 $ a \le \sqrt{n} $, \(O(n \sqrt{n} )\)枚举可过,构造方案的话往里面随便填 \(n\) 个#
即可 - 第二问:
一个凸的图形一定是由那个最小矩形挖去四个角得到的,且挖去的角一定是梯形
- 先 DP 求出面积为 \(i\) 的梯形的方案数,具体来讲:
\(g[i][j]\)表示面积为\(i\),一共有\(j\)列的梯形的方案数,我们规定梯形每一列的方块数单调不增
那要么新增一列,要么每一列都加一个方块:\(g[i][j]=g[i-1][j-1]+g[i-j][j]\) - 设\(sum[i]\)表示面积为\(i\)的梯形的方案数,则 \(sum[i]= \sum_{j = 1}^{i} g[i][j]\)
因为挖去的方块总数一定不会大于一条边长(否则完全可以缩小矩形),所以\(i,j<=\sqrt{n}\) - 然后 DP 四个角的情况:
设\(f[i][j]\)表示挖去\(j\)个角,一共挖去\(i\)个方块的方案数,则\(f[i][j]=f[i-k][j-1] \times sum[k]\)
还是因为挖去的方块总数一定不会大于一条边长,所以四个角一定不会相交。
但是比如当\(n=8\)时,\(2 \times 4\)和\(3 \times 3\)的边长一样是最小的,所以我们这里需要\(O(C)\)枚举边长,\(C\)表示周长。
注意这里千万不要\(O(n)\)枚举边长,因为当\(u=2\)时没有保证\(n \text{的和} \le 8 \times 10^5\)
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T,n,u,mod,g[N][N],sum[N],f[N][N];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
T=read(),u=read();
if(u==1){
while(T--){
n=read();
int a=sqrt(n),b=(n%a==0)?(n/a):(n/a+1);
printf("%d %d\n",a,b);
for(int i=1;i<=a;i++){
for(int j=1;j<=b;j++){
if((i-1)*b+j<=n) putchar('#');
else putchar('.');
}
puts("");
}
}
}
else{
mod=read();
g[0][0]=1;
for(int i=1;i<N;i++){
for(int j=1;j<=i;j++){
(g[i][j]=g[i-1][j-1]+g[i-j][j])%=mod;
}
}
for(int i=0;i<N;i++){
for(int j=0;j<=i;j++){
(sum[i]+=g[i][j])%=mod;
}
f[i][1]=sum[i];
}
for(int j=2;j<=4;j++){
for(int i=0;i<N;i++){
for(int k=0;k<=i;k++){
(f[i][j]+=1ll*f[i-k][j-1]*sum[k]%mod)%=mod;
}
}
}
while(T--){
n=read();
int a=sqrt(n),b=(n%a==0)?(n/a):(n/a+1),c=a+b,ans=0;
for(int i=1;i<=c;i++){
int j=c-i;
if(i*j>=n) (ans+=f[i*j-n][4])%=mod;
}
printf("%d %d\n",c*2,ans);
}
}
return 0;
}
4.AT_agc002_f [AGC002F] Leftmost Ball
AT_agc002_f [AGC002F] Leftmost Ball
因为每一种球有个数限定,为了避免麻烦,我们在每一次放一种颜色的球时选择把所有对应颜色的球都放进去,具体来讲:
设 \(f[i][j]\) 表示目前放了 \(i\) 个白球和 \(j\) 种颜色的球的方案数
一种合法的方案一定满足对于任意一个前缀白球数量大于等于放的颜色种类,即 \(i \ge j\)
对于当前放球的状态,我们找到第一个没有被放球的位置(空位和非空位可能交替出现):
- 如果放白球那就转移到 \(f[i+1][j]\)
注意放白球的时候我们并不把颜色种类\(j+1\) - 如果选择放不是白色的球 (此时需满足\(i>j\))
我们已经放了 \(j\) 种,还剩 \(n-j\) 种可以放的颜色球,除去那个白色的球和一定要放在这个位置上的球,每种球剩余\(k-2\) ,要在在剩余 $ n \times k - i - j \times (k-1) - 1$ 个位置里选出 \(k-2\) 个
则用 \(f[i][j] \times (n-j) \times C_{n \times k - i - j \times (k-1) - 1}^{k-2}\) 转移到 \(f[i][j+1]\)
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,mod=1e9+7,M=4e6+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,k,f[N][N];
int fac[M],inv[M],q[M];
int C(int n,int m){
return fac[n]*q[m]%mod*q[n-m]%mod;
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),k=read();
fac[0]=1;
for(int i=1;i<M;i++) fac[i]=fac[i-1]*i%mod;
inv[1]=1;
for(int i=2;i<M;i++)
inv[i]=(mod-mod/i)*inv[mod%i]%mod;
q[0]=1;
for(int i=1;i<M;i++)
q[i]=q[i-1]*inv[i]%mod;
f[1][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=i;j++){
(f[i+1][j]+=f[i][j])%=mod;
if(i>j) (f[i][j+1]+=f[i][j]*(n-j)%mod*C(n*k-i-j*(k-1)-1,k-2)%mod)%=mod;
}
}
if(k==1) printf("%lld\n",f[n][0]); //特判k=1
else printf("%lld\n",f[n][n]);
return 0;
}
5.CF1615F LEGOndary Grandmaster
先考虑怎么计算两个01串的距离
一个变换:把原串所有偶数位上的数取反,这样$ \text{取反两个原串中相同且相邻的数} = \text{交换两个新串中相邻的数} $
比如原串 从001101
变成000001
,
那么新串就从 011000
-> 010100
并且如果原串中,两个相邻的数不同,那它们在新串中就是相同的,交换他们和没交换一样
所以问题转化成:给定两个01串\(S\),\(T\) ,求最少的交换次数使得第一个串变成第二个
显然有解的充要条件是\(S,T\)中\(1\)的个数相同
设\(a_i\)表示\(S[1,i]\)中\(1\)的个数,\(b_i\)表示\(T[1,i]\)中\(1\)的个数,答案就是 \(\sum_{i=1}^{n} |ai-bi|\)
证明:
1.首先证明这是个下界,因为最终状态是 $ \sum_{i=1}^{n} |ai-bi| =0 $ 每一次交换 \(i\) 和 \(i+1\) ,只会影响 \(a\) 中 \(a_i\) 的值,且 \(|ai-bi|\) 的值至多 \(-1\) ,所以答案至少是 \(\sum_{i=1}^{n} |ai-bi|\)
2.可行性:\(|ai-bi|\)其实计算的就是最终跨过\(i\)这条分界线进行交换的\(1\)的个数
\({\color{red} {这个结论很重要}}\)
然后就是又一个套路:每个位置分别计算贡献
现在\(S,T\)是题目中的\(S,T\)了,即有?
号
设\(f[i][j]\)表示使得\(a_i-b_i=j\)的方案数,\(g[i][j]\)表示使得$s[i,n]中 \(1\) 的个数-t[i,n]中1的个数=j\(的方案数
这个转移\)O(n^2)\(很显然
所以\)ans=\sum_{i=1}^{n} \sum_{j=-n}^{n}f[i][j] \times g[i+1][-j] \times |j|$
因为要满足 \(S,T\) 中 \(1\) 个数相同
因为下标不可以是负数,所以要偏移量
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,mod=1e9+7;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T,n;
string s,t;
int a[N],b[N];
int f[N][N<<1],g[N][N<<1];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
T=read();
while(T--){
n=read();
cin>>s>>t;
for(int i=1;i<n;i+=2){
if(s[i]=='0') s[i]='1';
else if(s[i]=='1') s[i]='0';
if(t[i]=='0') t[i]='1';
else if(t[i]=='1') t[i]='0';
}
s=' '+s,t=' '+t;
for(int i=1;i<=n;i++){
if(s[i]!='?') a[i]=s[i]-'0';
if(t[i]!='?') b[i]=t[i]-'0';
}
for(int i=0;i<=n+1;i++){
for(int j=0;j<=2*n+2;j++){
f[i][j]=g[i][j]=0;
}
}
f[0][n]=1;
for(int i=1;i<=n;i++){
for(int j=-i;j<=i;j++){
if(s[i]!='?'&&t[i]!='?') f[i][j+n] = f[i-1][j - (a[i]-b[i]) + n];
else if(s[i]!='?'&&t[i]=='?') f[i][j+n] = ( f[i-1][j - (a[i]-0) + n] + f[i-1][j - (a[i]-1) + n])%mod;
else if(s[i]=='?'&&t[i]!='?') f[i][j+n] = ( f[i-1][j - (0-b[i]) + n] + f[i-1][j - (1-b[i]) + n])%mod;
else f[i][j+n] = ( ( f[i-1][j-1+n] + 2*f[i-1][j+n] ) % mod + f[i-1][j+1+n] )%mod;
}
}
g[n+1][n]=1;
for(int i=n;i>=1;i--){
for(int j=-(n-i+1);j<=n-i+1;j++){
if(s[i]!='?'&&t[i]!='?') g[i][j+n] = g[i+1][j - (a[i]-b[i]) + n];
else if(s[i]!='?'&&t[i]=='?') g[i][j+n] = ( g[i+1][j - (a[i]-0) + n] + g[i+1][j - (a[i]-1) + n])%mod;
else if(s[i]=='?'&&t[i]!='?') g[i][j+n] = ( g[i+1][j - (0-b[i]) + n] + g[i+1][j - (1-b[i]) + n])%mod;
else g[i][j+n] = ( ( g[i+1][j-1+n] + 2*g[i+1][j+n] ) % mod + g[i+1][j+1+n] )%mod;
}
}
int ans=0;
for(int i=1;i<=n;i++){
for(int j=-n;j<=n;j++){
(ans+=f[i][j+n]*g[i+1][-j+n]%mod*abs(j))%=mod;
}
}
printf("%lld\n",ans);
}
return 0;
}
6.CF1430G Yet Another DAG Problem
CF1430G Yet Another DAG Problem
因为 \(B_i>0\) 所以每条边 \(u \to v\), \(u\) 的权值大于 \(v\) 的权值
考虑给图分层,每一层的点的权值相同,层数越大的点点权越小,相邻层的权值差一定是\(1\) ,所以 \(u\) 的层数一定比 \(v\) 小
考虑状压 DP ,假设我们现在把点集 \(S\) 放到了前若干层,现在考虑下一层,假设下一层放的点集为\(T\),\(T\)需要满足:
- \(T \operatorname{and} S=0\),即 \(T\) 与 \(S\) 无交,相当于 \(T\) 是 \(S\) 补集的子集
- 任意一个在 \(T\) 中的 \(v\),所有指向他的 \(u\) 必须都在 \(S\) 里 (有了这个限制那也就必然满足所有 \(v\) 指向的点都不在 \(S\) 中)
考虑转移的代价:
对于每一个在 \(S\) 中的 \(u\) ,如果 \(u \to v\) 的 \(v\) 不在 \(S\) 中,这条边就会产生贡献
假设 \(u\) 在第 \(x\) 层,\(v\) 在第 \(y\) 层,那这条边的贡献应该是 \(w_i \times (y-x)\)
但是这样不是很好统计,所以我们考虑拆分贡献:
因为 \(v\) 至少在下一层,所以每次转移时我们把贡献加上:
\(\sum w_i\)
即所有满足 \(u\) 属于 \(S\) , \(v\) 不属于 \(S\) 的边 \((u,v)\) 的边权
这样的话如果在下一次转移时,这条边的 \(v\) 依旧没有被加入进来,那这条边又会产生一次 \(w_i\) 的贡献,直到 \(v\) 被加入进来,此时刚好产生 \(w_i \times (y-x)\) 的贡献
我们对每一个S预处理出:
- \(\sum wi \text{ (u,v)满足u属于S,v不属于S}\)
- 所有指向 \(S\) 中的点的点集
预处理复杂度\(O(n \times n \times 2^n)\),DP枚举 \(T\) 的时间复杂度 \(O(3^n)\)
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=(1<<18)+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m;
int tot,head[N],to[N],val[N],Next[N];
void add(int u,int v,int w){
to[++tot]=v,Next[tot]=head[u],val[tot]=w,head[u]=tot;
}
vector<int> G[N];
int sum[M],ru[M],f[M],from[M],ans[N];
void solve(int s,int val){
int tmp=s^from[s];
for(int u=1;u<=n;u++)
if((s>>(u-1))&1) ans[u]=val;
if(from[s]) solve(from[s],val+1);
}
void print(int s){
for(int i=1;i<=n;i++){
if(s>>(i-1)&1) cout<<i<<' ';
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),m=read();
for(int i=1;i<=m;i++){
int u=read(),v=read(),w=read();
add(u,v,w);
G[v].push_back(u); //记录入边
}
for(int s=0;s<(1<<n);s++){
for(int u=1;u<=n;u++){
if((s>>(u-1))&1){
for(int i=head[u];i;i=Next[i]){
int v=to[i],w=val[i];
if(!((s>>(v-1))&1)) sum[s]+=w;
}
for(int v:G[u]){
ru[s]|=(1<<(v-1));
}
}
}
}
memset(f,0x3f,sizeof f);
f[0]=0;
for(int s=0;s<(1<<n);s++){
int S=s^((1<<n)-1); //计算补集
for(int t=S;t;t=(t-1)&S){ //枚举t
if((ru[t]&s)==ru[t]){ //任意一个在T中的v,所有指向他的u必须都在S里
if(f[s]+sum[s]<f[t|s])
f[t|s]=f[s]+sum[s],from[t|s]=s;
}
}
}
solve((1<<n)-1,0);
for(int i=1;i<=n;i++) printf("%d ",ans[i]);
puts("");
return 0;
}
7.CF1648D Serious Business
用 \(pre_{1/2/3}\)记录每一行的前缀和
设 \(f[x]\) 表示从 \((1,1)\) 到 \((2,x)\) 的最大价值,
则 \(ans=\max(f[x]+pre_3[n]-pre_3[x-1])\)
考虑每一个操作 \((l_i,r_i,k_i)\) 对 \(f[x]\) 的贡献
显然对于每一个 \(l_i \le y \le x\) 我都可以按以下路径走: \((1,1) \to (1,y) \to (2,y) \to (2,x)\)
即
对于每一个 \(l_i-1 \le y \le x\) 我也可以按照以下路径走:
\((1,1) \to (2,y) \to (2,x)\)
即
注意:这里 \(a[2][y]\) 在 \(f[y]\) 中算过了,所以是 \(pre_2[y]\) ,不是 \(pre_2[y-1]\)
对于第二个转移还可以精简:
如果 \(l_i \le y \le x\) , 那路径必然是:
\((1,1) \to (1,z) \to (2,z) \to (2,y) \to (2,x)\)
其中 \(1 \le z \le y\)
- 如果 \(li \le z \le y\),这条路径
\((1,1) -> (1,z) -> (2,z) -> (2,x)\)
在转移1中已经转移过了 - 如果 \(z<l_i\),这条路径再细分:
\((1,1) \to (1,z) \to (2,z) \to (2,l_i-1) \to (2,x)\)
而这个其实就是 \(f[l_i-1]+pre_2[x]-pre_2[l_i-1-1]-k_i\)
所以转移二其实只需要把 \(f[x]\) 和 \(f[l_i-1]+pre_2[x]-pre_2[l_i-1]-k_i\) 取 \(max\)
综上得到转移方程为:
显然可以先转移后半部分,再转移前半部分,并且前半部分的转移可以直接线段树区间取 \(max\)
现在讨论后半部分的转移
后半部分的转移需满足 \(l_i \le y \le x \le r_i\) , 很讨厌 ,想办法搞掉一个 \(r_i\)
可以从后往前枚举 \(x\) , 并不断加入区间,这样就满足了\(r_ i\) 的条件 (但不一定满足 \(l_i\) )
线段树上维护三个值 \(max_A\) , \(max_B\) , \(max_{(A+B)}\)
- \(max_A\):表示对应区间上最大的 \(-k_i\)
- \(max_B\):表示对应区间上最大的 \(pre_1[y] - pre_2[y-1]\)
- \(max_{(A+B)}\):表示对应区间上满足的最大的 \(-k_i + pre_1[y] - pre_2[y-1]\) (并且需要满足 \(-k_i\) 对应的\(l_i\) 在 \(pre_1[y] - pre_2[y-1]\) 对应的 \(y\) 之前)
于是只要在区间 \([1,x]\) 上查询 \(max_{(A+B)}\)即可
对于 \(-k_i\) 可以在往前扫的时候插入,对于 \(pre_1[y] - pre_2[y-1]\) 可以提前就插入
线段树合并时就先合并 \(max_A\) 和 \(max_B\)
对于 \(max_{(A+B)}\) : 先继承左右儿子的 \(max_{(A+B)}\),再用左儿子的 {max_A} 加上 右儿子的 {max_B}
两部分转移各需要一棵线段树
\({\color{green} {插一嘴,真的难写}}\)
code
#include<bits/stdc++.h>
#define int long long
#define PIII pair<pair<int,int>,int>
#define fi first
#define se second
using namespace std;
const int N=5e5+5,inf=0x3f3f3f3f3f3f3f3f;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,q,a[5][N],pre[5][N],f[N];
struct P{
int l,r,k;
}b[N];
struct node1{
int l,r,max_A,max_B,max_sum;
};
struct SegmentTree1{
node1 t[N<<2];
void pushup(int p){
t[p].max_A=max(t[p<<1].max_A,t[p<<1|1].max_A);
t[p].max_B=max(t[p<<1].max_B,t[p<<1|1].max_B);
t[p].max_sum=max({t[p<<1].max_sum,t[p<<1|1].max_sum,t[p<<1].max_A+t[p<<1|1].max_B});
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r){
t[p].max_A=t[p].max_B=t[p].max_sum=-inf;
return;
}
int mid=(t[p].l+t[p].r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pushup(p);
}
void change_A(int p,int x,int val){
if(t[p].l==t[p].r){
t[p].max_A=max(t[p].max_A,val);
t[p].max_sum=max(t[p].max_sum,t[p].max_A+t[p].max_B);
return;
}
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) change_A(p<<1,x,val);
else change_A(p<<1|1,x,val);
pushup(p);
}
void change_B(int p,int x,int val){
if(t[p].l==t[p].r){
t[p].max_B=max(t[p].max_B,val);
t[p].max_sum=max(t[p].max_sum,t[p].max_A+t[p].max_B);
return;
}
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) change_B(p<<1,x,val);
else change_B(p<<1|1,x,val);
pushup(p);
}
PIII ask(int p,int l,int r){
if(l<=t[p].l&&t[p].r<=r) return {{t[p].max_A,t[p].max_B},t[p].max_sum};
int mid=(t[p].l+t[p].r)>>1;
if(r<=mid) return ask(p<<1,l,r);
if(l>mid) return ask(p<<1|1,l,r);
PIII res1=ask(p<<1,l,r),res2=ask(p<<1|1,l,r);
int max_A=max(res1.fi.fi,res2.fi.fi),max_B=max(res1.fi.se,res2.fi.se),max_sum=max({res1.se,res2.se,res1.fi.fi+res2.fi.se});
return {{max_A,max_B},max_sum};
}
}T1;
struct node2{
int l,r,maxn,lazy;
void tag(int val){
maxn=max(maxn,val);
lazy=max(lazy,val);
}
};
struct SegmentTree2{
node2 t[N<<2];
void pushup(int p){
t[p].maxn=max(t[p<<1].maxn,t[p<<1|1].maxn);
}
void spread(int p){
if(t[p].lazy!=-inf){
t[p<<1].tag(t[p].lazy);
t[p<<1|1].tag(t[p].lazy);
t[p].lazy=-inf;
}
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r,t[p].lazy=-inf;
if(l==r){
t[p].maxn=f[l];
return;
}
int mid=(t[p].l+t[p].r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pushup(p);
}
void change(int p,int l,int r,int val){
if(l<=t[p].l&&t[p].r<=r){
t[p].tag(val);
return;
}
spread(p);
int mid=(t[p].l+t[p].r)>>1;
if(l<=mid) change(p<<1,l,r,val);
if(r>mid) change(p<<1|1,l,r,val);
pushup(p);
}
int ask(int p,int x){
if(t[p].l==t[p].r) return t[p].maxn;
spread(p);
int mid=(t[p].l+t[p].r)>>1;
if(x<=mid) return ask(p<<1,x);
else return ask(p<<1|1,x);
}
}T2;
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),q=read();
for(int i=1;i<=3;i++){
for(int j=1;j<=n;j++){
a[i][j]=read();
pre[i][j]=pre[i][j-1]+a[i][j];
}
}
for(int i=1;i<=q;i++) b[i].l=read(),b[i].r=read(),b[i].k=read();
T1.build(1,1,n);
for(int i=1;i<=n;i++)
T1.change_B(1,i,pre[1][i] - pre[2][i-1]);
sort(b+1,b+q+1,[](const P&x,const P&y){return x.r>y.r;});
int i=1;
for(int x=n;x>=1;x--){
while(i<=q&&b[i].r>=x) T1.change_A(1,b[i].l,-b[i].k),i++;
f[x]=T1.ask(1,1,x).se; //先别加pre[2][x]
}
T2.build(1,1,n); //建树的时候直接按照f数组
sort(b+1,b+q+1,[](const P&x,const P&y){return x.l<y.l;});
for(int i=1;i<=q;i++){
int l=b[i].l,r=b[i].r;
if(l>1){
int tmp=T2.ask(1,l-1);
T2.change(1,l,r,tmp-b[i].k);
//注意这个时候因为我们的终点并不是(2,l-1),f[l-1]所以不能加上pre[2][l-1],直接用原来没有加过pre2的f值是对的
}
//这个转移的意义是先走到(2,l-1),所以l!=1
}
for(int i=1;i<=n;i++) f[i]=T2.ask(1,i)+pre[2][i];
int ans=-inf;
for(int i=1;i<=n;i++){
ans=max(ans,f[i]+pre[3][n]-pre[3][i-1]);
}
printf("%lld\n",ans);
return 0;
}
8.CF367E Sereja and Intervals
一个关于互不包含区间的结论:
如果把区间左端点升序排序,则区间右端点也必然升序
也就是说当你确认了 \(n\) 个 \(l_i\) ,和 \(n\) 个 \(r_i\),那他们的组合方法是唯一的.
所以你现在要构造两个序列 {\(l_i\)},{\(r_i\)} ,满足:
- \(1 \le l_1 < l_2 < l_3 < l_4 < l_5 < ...< l_n \le m\)
- $ 1 \le r_1 < r_2 < r_3 < r_4 < r_5 < ... < r_n<=m $
- \(l_i \le r_i\)
- \(\text{存在} l_i=x\)
考虑几个性质:
- 不可能存在一个点存在两个左端点/右端点
- \(n>m\) 时无解,所以可以认为 \(n \le m\) , 即 \(n \le \sqrt {10^5}\)
- 3限制等价于对于每一个\(i\),其左边左端点 \(cnt(l,i)\) 的数量 \(\ge\) 其左边右端点的数量 \(cnt(r,i)\)
如果没有限制4,考虑 DP :
\(f[i][j][k]\) 表示给前 \(i\) 个位置分配 \(j\) 个 \(l\) 端点,\(k\) 个 \(r\) 端点的方案数 \((j \ge k)\) 。
转移时,每一个位置你要么分配一个 \(l\) ,要么分配一个 \(r\) ,要么分配 \(l\) , \(r\) 各一个,要么啥也不分配
时间复杂度\(O(m \times n^2)\)
如果有限制4,其实只需要在 \(i=x\) 时,强制只转移有放左端点的情况
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5,mod=1e9+7;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,x;
int f[2][400][400]; //滚动数组
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),m=read(),x=read();
if(n>m){
printf("0\n");
return 0;
}
f[0][0][0]=1;
for(int i=1;i<=m;i++){
for(int j=0;j<=min(i,n);j++){
for(int k=0;k<=j;k++){
if(j-1>=k&&j-1>=0) (f[i&1][j][k]+=f[1-(i&1)][j-1][k])%=mod;
if(i!=x&&k-1>=0) (f[i&1][j][k]+=f[1-(i&1)][j][k-1])%=mod;
if(j-1>=0&&k-1>=0) (f[i&1][j][k]+=f[1-(i&1)][j-1][k-1])%=mod;
if(i!=x) (f[i&1][j][k]+=f[1-(i&1)][j][k])%=mod;
}
}
for(int j=0;j<=min(n,i);j++){
for(int k=0;k<=j;k++){
f[1-(i&1)][j][k]=0;
}
}
}
int fac=1;
for(int i=1;i<=n;i++) (fac*=i)%=mod; //区间有编号所以乘以n!
printf("%lld\n",f[m&1][n][n]*fac%mod);
return 0;
}
9.CF1574F Occurrences
这个限制是真的抽象,而且这题里的子序列指子串,为了避免误会下面直接写子串
考虑转化限制:
仔细想一想可以发现 \(A\) 在序列 \(a\) 中每出现一次,
其每个子串都会出现一次,且出现次数最多的一定是长度为\(1\)的单个字符。
所以这个限制其实就是:\(A\) 在 \(a\) 中出现的次数 \(=\) 其每个字符在 \(a\) 中出现的次数。
考虑几种情况:
- 如果\(A=\text{123}\),此时当 \(a\) 中填了一个
1
时,必定要按照顺讯再填进去2
,3
即 \(a\) 中要么没有1
,有的话一定是以123
的形式出现的。
如果连边的话会发现此时 \(1 \to 2 \to 3\)构成一条链。 - 如果\(A=\text{121}\),会发现此时无论如何填
1
出现的次数一定大于 \(A\),除非不填1
。
而此时连边会出现环,即有环的一定不能填。 - 如果\(A=\text{123},B=\text{234}\),此时要把 \(A\) 和 \(B\) 的链合并,变成
1234
。 - 如果\(A=\text{123},B=\text{124}\),此时出现了分支,同样不能填。
于是一个做法就成型了:
- 一开始每个数字自成一个连通块。
- 对每个序列 \(A\) ,按照顺序连边。
- 对每一个连通块,如果它存在环或者存在分支(即它不是链),那这个连通块内的数都不能出现在 \(a\) 中。
否则这条链可以出现在a中 - 对每一个可以出现的链做一次完全背包,即如果长度为 \(j\) 的链有 \(w_j\) 个,\(f[i]\) 表示构造长度为 \(i\) 的 \(a\) 的方案数,有\(f[i]=\sum_{j=1}^i f[i-j] \times w_j\)
这样转移可以大大加快速度,因为枚举每个链可能有 \(O(k)\)个,但不同的链长一定小于等于 \(O(\sqrt {k})\)个
因为假设不同链长有 \(l\) 个,那即使这些链的长度为\(1,2,3,4,...,l\)
则 \((1+2+3+..+l)=\frac {(1+l) \times l}{2} \le k\),即$ l \le \sqrt{2k}$
code
#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=3e5+5,mod=998244353;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,k,a[N];
map<PII,bool> mp;
int tot,head[N],to[N],Next[N],ru[N],chu[N];
void add(int u,int v){
to[++tot]=v,Next[tot]=head[u],head[u]=tot;
ru[v]++,chu[u]++;
}
int num,c[N],siz[N];
bool vis[N],flag[N];
void dfs(int u){
siz[num]++;
vis[u]=true,c[u]=num;
for(int i=head[u];i;i=Next[i]){
int v=to[i];
if(vis[v]) flag[num]=false;
else dfs(v);
}
}
set<int> len;
int w[N],f[N];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),m=read(),k=read();
memset(flag,true,sizeof flag);
for(int i=1;i<=n;i++){
int c=read(),lst;
for(int j=1;j<=c;j++){
a[j]=read();
if(j>1&&!mp[{lst,a[j]}]) add(lst,a[j]),mp[{lst,a[j]}]=true;;
lst=a[j];
}
}
for(int i=1;i<=k;i++){
if(!vis[i]&&ru[i]==0) num++,dfs(i);
}
for(int i=1;i<=k;i++)
if(ru[i]>1||chu[i]>1) flag[c[i]]=false;
for(int i=1;i<=num;i++)
if(flag[i]) len.insert(siz[i]),w[siz[i]]++;
f[0]=1;
for(int i=1;i<=m;i++){
for(int x:len) if(i>=x) (f[i]+=f[i-x]*w[x])%=mod;
}
printf("%lld\n",f[m]);
return 0;
}
10.P5662 [CSP-J2019] 纪念品
因为当日购买的纪念品也可以当日卖出换回金币。
所以如果想保留一个纪念品可以看成是:
第一天买,第二天早上卖,第二天再买回,第三天早上卖,第三天再买回...
这样就不用管每一天手上有多少纪念品,只需要认为我当天买的第二天一早一定会直接卖掉,啥也不剩,至于再买不买回来是第二天的事 。
设 \(A_{i,j}\) 表示第 \(i\) 天 \(j\) 物品的价格。
假设我考虑到了第 \(i\) 天,手里剩 \(M\) 元 , 买入一个物品需要花 \(A_{i,j}\) 元,收益是 \(A_{i+1,j} - A_{i,j}\)。
可以看成是有一个体积为 \(M\) 的背包,每个物品的体积为 \(A_{i,j}\) ,价值是 \(A_{i+1,j} - A_{i,j}\) 的完全背包
最后按照当天获得的最大价值当做下一天的起始资金即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T,n,M,a[105][105];
int f[N];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
T=read(),n=read(),M=read();
for(int i=1;i<=T;i++){
for(int j=1;j<=n;j++){
a[i][j]=read();
}
}
for(int i=1;i<T;i++){
for(int k=0;k<=M;k++) f[k]=0;
for(int j=1;j<=n;j++){
for(int k=a[i][j];k<=M;k++){
f[k]=max(f[k],f[k-a[i][j]]+a[i+1][j]-a[i][j]);
}
}
M=M+f[M]; //因为f数组算的是可以收益多少,所以M直接+f[M]
}
printf("%d\n",M);
return 0;
}
11.P2886 [USACO07NOV] Cow Relays G
P2886 [USACO07NOV] Cow Relays G
矩阵快速幂优化 DP 板子
我们用一个矩阵表示两两之间的答案,一开始 \(A[i][j]\) 表示 \(i\) , \(j\) 只经过一条边的最短路
把矩阵乘法改成 \(C[i][j] = min(A[i][k]+A[k][j])\)
这里 k 相当于枚举了中转点 (参考Floyd)
并且此时 路径长度会变成2倍,最终要求是 n 倍,做矩阵快速幂即可
code
#include<bits/stdc++.h>
#define int long long
#define PIII pair<int,pair<int,int> >
#define fi first
#define se second
using namespace std;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int dis[10005],N,n,m,s,t;
vector<PIII> G;
int Dis(int x){
return lower_bound(dis+1,dis+n+1,x)-dis;
}
int ans[1005][1005],c[1005][1005];
void mul(int f[1005][1005],int a[1005][1005]){ //矩阵乘法
memset(c,0x3f,sizeof c);
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
c[i][j]=min(c[i][j],f[i][k]+a[k][j]);
}
}
}
memcpy(f,c,sizeof c);
}
void Quick_power(int a[1005][1005],int b){
for(int i=1;i<=n;i++){ //单位矩阵
for(int j=1;j<=n;j++){
if(i==j) ans[i][j]=0;
else ans[i][j]=0x3f3f3f3f3f3f3f3f;
}
}
while(b){
if(b&1) mul(ans,a);
b>>=1,mul(a,a);
}
memcpy(a,ans,sizeof ans);
}
int a[1005][1005];
signed main(){
N=read(),m=read(),s=read(),t=read();
memset(a,0x3f,sizeof a);
for(int i=1;i<=m;i++){
int w=read(),u=read(),v=read();
G.push_back({u,{v,w}});
dis[i]=u,dis[i+m]=v;
}
sort(dis+1,dis+2*m+1);
n=unique(dis+1,dis+2*m+1)-dis-1;
for(PIII x:G){
int u=x.fi,v=x.se.fi,w=x.se.se;
u=Dis(u),v=Dis(v);
a[u][v]=a[v][u]=w;
}
Quick_power(a,N);
printf("%lld\n",a[Dis(s)][Dis(t)]);
return 0;
}
12.P6569 [NOI Online #3 提高组] 魔法值
相同的转移情况再次想到矩阵快速幂。
构造转移矩阵 \(G\) , 若 \(u\),\(v\) 之间有边,则 \(G[u][v]=1\) , 否则 \(G[u][v]=0\)
将矩阵乘法改成 \(C[i][j] = \operatorname{xor} ^ n _ {k=1} A[i][k] \times B[k][j]\)
注意:广义矩乘若要满足结合律必须满足——加法满足交换律,乘法满足结合律,并对加法满足分配率,而普通的异或对加法是没有分配律的,也不能这么改变,但由于这里只有 \(0/1\) 所以可以
直接快速幂的总时间复杂度是 $ O(q \times n^3 * \log a)$ 过不了。
所以考虑预处理出 \(G^1,G^2,G^4,......\) ,并对 \(a\) 进行二进制拆分,这样每一次乘都是一个 \(n \times n\)矩阵乘以 \(1 \times n\)的向量,时间复杂度变成 $ O(q \times n^2 * \log a)$
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,q;
struct matrix{
int x[105][105];
int n,m;
}f,a,mi[40];
matrix mul(matrix x,matrix y){
matrix ans;
ans.n=x.n,ans.m=y.m;
memset(ans.x,0,sizeof ans.x);
for(int k=1;k<=x.m;k++){
for(int i=1;i<=ans.n;i++){
for(int j=1;j<=ans.m;j++){
ans.x[i][j]^=(x.x[i][k]*y.x[k][j]);
}
}
}
return ans;
}
signed main(){
n=read(),m=read(),q=read();
f.n=1,f.m=n;
a.n=n,a.m=n;
for(int i=1;i<=n;i++) f.x[1][i]=read();
for(int i=1;i<=m;i++){
int u=read(),v=read();
a.x[u][v]=a.x[v][u]=1;
}
mi[0]=a;
for(int i=1;i<=32;i++) mi[i]=mul(mi[i-1],mi[i-1]);
while(q--){
int t=read();
matrix ans=f;
for(int i=0;i<=32;i++){
if(t>>i&1) ans=mul(ans,mi[i]);
}
printf("%lld\n",ans.x[1][1]);
}
return 0;
}
13.P6190 [NOI Online #1 入门组] 魔法
一条合法的 \(1 \to n\) 路径可以拆成两部分:
- 一开始没有任何魔法的路径
- 若干段满足:第一条路径用了魔法,后面没有用的路径
并且如果可以用完一定会把 \(k\) 次魔法用完。
所以 \(A[i][j]\) 表示 \(i\) 到 \(j\) 用一次魔法且用在第一条的最短距离。
矩阵快速幂到 \(A^k\) 即为用了 \(k\) 次魔法。
注意还要用Floyd跑出全源最短路,处理出那些没有任何魔法的路径的最小值。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,inf=1e15;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,k;
void Init(int a[105][105]){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i!=j) a[i][j]=inf; //这里特殊判断 i!=j , 防止 1--n 路径上可能不足 k 条边的情况
}
}
}
int ans[105][105],c[105][105];
void mul(int f[105][105],int a[105][105]){ //矩阵乘法
Init(c);
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
c[i][j]=min(c[i][j],f[i][k]+a[k][j]);
}
}
}
memcpy(f,c,sizeof c);
}
void Quick_power(int a[105][105],int b){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j) ans[i][j]=0;
else ans[i][j]=inf;
}
}
while(b){
if(b&1) mul(ans,a);
b>>=1,mul(a,a);
}
memcpy(a,ans,sizeof ans);
}
int f[105][105],a[105][105];
int tot,head[N],to[N],Next[N],val[N];
void add(int u,int v,int w){
to[++tot]=v,Next[tot]=head[u],val[tot]=w,head[u]=tot;
}
signed main(){
n=read(),m=read(),k=read();
Init(f);
for(int i=1;i<=n;i++) f[i][i]=0;
for(int i=1;i<=m;i++){
int u=read(),v=read(),w=read();
f[u][v]=w;
add(u,v,w);
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
}
}
if(k==0){
printf("%lld\n",f[1][n]);
return 0;
}
Init(a);
for(int u=1;u<=n;u++){
for(int v=1;v<=n;v++){
for(int i=head[u];i;i=Next[i]){
int w=val[i];
a[u][v]=min(a[u][v],-w+f[to[i]][v]);
}
}
}
Quick_power(a,k);
int res=inf;
for(int u=1;u<=n;u++){
res=min(res,f[1][u]+a[u][n]);
}
printf("%lld\n",res);
return 0;
}
14.P6772 [NOI2020] 美食家
朴素 DP : \(f[i][u]\) 表示第 \(i\) 天到 \(u\) 的最大收益。
则 \(f[i][u] = max(f[i-w][v]+c[u])\) (存在一条边 \((v,u,w)\) )
因为 \(T\) 很大,考虑矩阵快速幂优化:
由于矩阵快速幂一般只能优化从 \(f[i] \to f[i+1]\) 的转移,所以考虑拆点。
Tip:之所以不拆边是因为 n 比较小
即把一个点 \(u\) 拆成 $ u_1,u_2,u_3,u_4,u_5$
并且按照 \(u_1 \to u_2 \to u_3 \to u_4 \to u_5\) 连边
若存在一条边 \((u,v,3)\) 则按照 \(u_3 -> v_1\) 连边
相当于变成新图中经过了多少条边就是几天。
并且只在所有节点分裂出的第一个点,即 \(u_1\) 上 $c[ u_1 ]= c[u] $, 其余点的 \(c\) 值均为 \(0\)。
设计转移矩阵 \(G\) ,若新图中 \(u\) 和 \(v\) 之间有边,
则 \(G[u][v] = c[v]\),否则 \(G[u][v] = -inf\) 。
改变矩乘定义:
$ C[i][j] = \max{A[i][k] + B[k][j]} $。
一开始除了 \(f[1][1]=c[1]\) 其余均为 \(-inf\)。
若不考虑美食节 , 则 答案 \(=Ans[1][1]\) ,其中 \(Ans=f \times G^T\)
对于有美食节的情况:
在每个美食节做一次朴素转移,具体来说
因为每个美食节不在一个一个时间举行,先按照每个美食节的时间排序。
对于 \(t[i-1]\) 到 \(t[i]\) :
则 \(f = f * G ^ {t[i] - t[i-1]}\)
之后再特殊将 \(f[1][x[i] ] = f[1][x[i]] + y[i]\)。
若最后 \(t[k] \ne T\) 则再将 \(f = f * G ^ (T-t[k])\)
但是时间复杂度为 \(O(k \times (5n) ^ 3 \times \log T)\)
为了优化复杂度,我们还是考虑二进制拆分优化,参见 P6569 [NOI Online #3 提高组] 魔法值
将所有 \(G^1,G^2,G^4 ....\) 预处理出来,预处理时间复杂度 \(O((5n) ^ 3 * \log T)\)。
每一次二进制拆分 \(t[i]-t[i-1]\) , 依次乘上对应的矩阵。
由于这里每一次乘都是一个 \(1 \times 5n\) 的向量 \(f\) 乘上一个 \(5n \times 5n\) 的矩阵 \(G\) , 时间复杂度只有 \(O(k * (5n) ^ 2 \times logT)\)
总时间复杂度 \(O((5n) ^ 3 \times \log T + k \times (5n) ^ 2 \times \log T)\)
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,inf=5e15;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,T,k;
int c[N];
struct Festival{
int t,x,y;
}a[N];
bool edge[300][300];
void add(int u,int v){
edge[u][v]=true;
}
struct Matrix{
int a[300][300];
int n,m;
void Init(int val){
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j]=val;
}
}
}
}G,F,mi[32];
Matrix operator * (const Matrix &A,const Matrix &B){
Matrix C;
C.n=A.n,C.m=B.m;
C.Init(-inf);
for(int k=1;k<=A.m;k++){
for(int i=1;i<=C.n;i++){
for(int j=1;j<=C.m;j++){
C.a[i][j]=max(C.a[i][j],A.a[i][k]+B.a[k][j]);
}
}
}
return C;
}
signed main(){
n=read(),m=read(),T=read(),k=read();
for(int i=1;i<=n;i++){
c[i]=read();
for(int j=1;j<=4;j++){
add(i+(j-1)*n,i+j*n);
}
}
for(int i=1;i<=m;i++){
int u=read(),v=read(),w=read();
add(u+(w-1)*n,v);
}
for(int i=1;i<=k;i++){
a[i].t=read(),a[i].x=read(),a[i].y=read();
}
F.n=1,F.m=5*n;
G.n=5*n,G.m=5*n;
F.Init(-inf);
F.a[1][1]=c[1];
for(int i=1;i<=5*n;i++){
for(int j=1;j<=5*n;j++){
if(edge[i][j]) G.a[i][j]=c[j];
else G.a[i][j]=-inf;
}
}
mi[0]=G;
for(int i=1;i<=30;i++) mi[i]=mi[i-1]*mi[i-1];
sort(a+1,a+k+1,[](Festival x,Festival y){return x.t<y.t;});
a[0].t=0;
for(int i=1;i<=k;i++){
int t=a[i].t-a[i-1].t;
for(int j=0;j<=30;j++)
if(t>>j&1) F=F*mi[j];
F.a[1][a[i].x]+=a[i].y;
}
if(a[k].t!=T){
int t=T-a[k].t;
for(int j=0;j<=30;j++)
if(t>>j&1) F=F*mi[j];
}
if(F.a[1][1]<0) printf("-1\n");
else printf("%lld\n",F.a[1][1]);
return 0;
}
15.CF1051E Vasya and Big Integers
CF1051E Vasya and Big Integers
设 \(f[i]\) 表示划分后 \([i,n]\) 的方案。
能从 \(f[j]\) 转移到 \(f[i]\) 的 \(j\) 一定在 \([i+lenL,i+lenR]\) 中的一段连续区间 ( \(lenR\) 为 \(r\) 的长度,\(lenL\) 为 \(l\) 的长度)。
事实上只有 \(j=i+lenR\) 和 \(j=i+LenL\) 时有可能不合法,特殊判断一下(二分+ hash 或 ex_KMP) 即可。
转移时用后缀和优化。
之所以这么设计状态是因为这样基本不用考虑前导零。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,mod=998244353;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
string s,a,b;
int n,lenL,lenR;
int z[N],p1[N],p2[N];
int f[N],q[N];
void Init(){
int l=0,r=0;
z[1]=lenL;
for(int i=2;i<=lenL;i++){
if(i<=r) z[i]=min(r-i+1,z[i-l+1]);
while(a[1+z[i]]==a[i+z[i]]) z[i]++;
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
l=0,r=0;
for(int i=1;i<=n;i++){
if(i<=r) p1[i]=min(r-i+1,z[i-l+1]);
while(1+p1[i]<=lenL&&i+p1[i]<=n&&s[i+p1[i]]==a[1+p1[i]]) p1[i]++;
if(i+p1[i]-1>r) l=i,r=i+p1[i]-1;
}
memset(z,0,sizeof z); //清空!!!!!!
l=0,r=0;
z[1]=lenR;
for(int i=2;i<=lenR;i++){
if(i<=r) z[i]=min(r-i+1,z[i-l+1]);
while(b[1+z[i]]==b[i+z[i]]) z[i]++;
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
l=0,r=0;
for(int i=1;i<=n;i++){
if(i<=r) p2[i]=min(r-i+1,z[i-l+1]);
while(1+p2[i]<=lenR&&i+p2[i]<=n&&s[i+p2[i]]==b[1+p2[i]]) p2[i]++;
if(i+p2[i]-1>r) l=i,r=i+p2[i]-1;
}
}
bool cmp1(int l,int r){
if(r-l+1<lenL) return false;
if(r-l+1>lenL) return true;
if(p1[l]==lenL) return true;
return a[1+p1[l]]<=s[l+p1[l]];
}
bool cmp2(int l,int r){
if(r-l+1<lenR) return true;
if(r-l+1>lenR) return false;
if(p2[l]==lenR) return true;
return b[1+p2[l]]>=s[l+p2[l]];
}
bool check(int l,int r){
if(l>r) return false;
return cmp1(l,r)&&cmp2(l,r);
}
signed main(){
// freopen("cyq.in","r",stdin);
// freopen("cyq.out","w",stdout);
cin>>s>>a>>b;
n=s.size(),lenL=a.size(),lenR=b.size();
s=' '+s,a=' '+a,b=' '+b;
Init();
f[n+1]=1;
q[n+1]=1;
for(int i=n;i>=1;i--){
if(s[i]=='0'){
if(check(i,i)) f[i]=f[i+1];
}
else{
int l=min(n+1,i+lenL),r=min(n+1,i+lenR);
if(!check(i,l-1)) l++;
if(!check(i,r-1)) r--;
if(l<=r) f[i]=(q[l]-q[r+1]+mod)%mod;
else f[i]=0;
}
q[i]=(q[i+1]+f[i])%mod;
}
printf("%lld\n",f[1]);
return 0;
}
16.CF1310C Au Pont Rouge
所有子串排序后二分答案。
check 相当于要求把 \(S\) 分割成 \(m\) 段大小都大于一个给定子串的方案数,转移和上题类似 。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,cnt,k;
string s;
int lcp[1005][1005];
struct P{
int l,r;
}a[N];
bool operator > (P const &x, P const &y) {
int L=lcp[x.l][y.l];
if (L>=x.r-x.l+1||L>=y.r-y.l+1)
return x.r-x.l+1>y.r-y.l+1;
return s[x.l+L]>s[y.l+L];
}
int f[1005][1005],q[1005][1005];
// f[j][i] 表示把 [i,n] 划分成 j 使每一段都比给定的大,q[j][i]表示对于 j 的后缀和。
int check(P x){
memset(f,0,sizeof f);
memset(q,0,sizeof q);
f[0][n+1]=1;
for(int i=n+1;i>=1;i--) q[0][i]=q[0][i+1]+f[0][i];
int l=x.l,r=x.r,len=r-l+1;
for(int j=1;j<=m;j++){
for(int i=n;i>=1;i--){
int tmp=min(len,lcp[i][l]);
if(tmp==len||s[i+tmp]>=s[l+tmp])
f[j][i]=q[j-1][i+tmp+1];
}
for(int i=n;i>=1;i--) q[j][i]=min((long long)1e18,q[j][i+1]+f[j][i]); //防止爆掉
}
return f[m][1];
}
signed main(){
n=read(),m=read(),k=read();
cin>>s; s=' '+s;
lcp[n][n]=1;
for(int i=n;i>=1;i--){
for(int j=n;j>=1;j--){
if(i==j&&i==n) continue;
if(s[i]==s[j]) lcp[i][j]=lcp[i+1][j+1]+1;
else lcp[i][j]=0;
}
}
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
a[++cnt]={i,j};
}
}
sort(a+1,a+cnt+1,greater<P>());
int l=1,r=cnt,mid,ans;
while(l<=r){
mid=(l+r)>>1;
if(check(a[mid])+1<=k) ans=mid,l=mid+1;
else r=mid-1;
}
for(int i=a[ans].l;i<=a[ans].r;i++){
printf("%c",s[i]);
}
printf("\n");
return 0;
}
17.CF477D Dreamoon and Binary
-
方案数:
一个很好想的 DP , 设 \(f[i][j]\) 表示最后一段为 \([j,i]\) , 划分 \([1,i]\) 的方案数。
则 \(f[i][j]=\sum_{k=j-1-len+1}^{j-1} f[j-1][k]\) ,其中 $ len=i-j+1$ 。
转移用前缀和优化即可做到 \(O(n^2)\)。
注意当 \(k=j-1-len+1\) 即 \(s[k,j-1]\) 和 \(s[j,i]\) 长度一样时要判断前一个是否比后一个小 , 可以预处理 LCP
对于前导\(0\)的处理只需要在\(0\)的位置不算入前缀和即可。 -
最小操作次数:
乍一看取模之后似乎是不能比较大小的,所以就不能 DP ,所以分析性质。
考虑最后的答案是怎么算的:
\(ans = \text{打印次数+加一次数} = m+val\)。
其中 \(m\) 为段数, \(val\) 为最后一段的值。
假设最后一段为 \(s[i,n]\)。
当 \(i\) 前移时, \(m\) 只会最多减少 \(1\) , 而 \(val\) 会多一个数量级,而 \(m \le 5000 < 2^{17}\),
所以我们其实只需要考虑 \(n-16 \le i\le n\) 的答案即可,如果这个位置的划分不合法就是第一问中 f[n][i]=0 。
我们只需要 DP 求出最小的段数即可,这里由于 \(j \le i\) ,所以 DP 时记录一下后缀最小值 , 就可以也做到 \(O(n^2)\)
Tip:当然如果这个区间里无解还要继续往前。(见代码)
别 define int long long
,卡空间
code
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=1e5+5,inf=0x3f3f3f3f;
const ll mod=1e9+7;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
string s;
int n;
int lcp[5005][5005];
void Init(){
for(int i=n;i>=1;i--){
for(int j=n;j>=1;j--){
if(s[i]==s[j]) lcp[i][j]=lcp[i+1][j+1]+1;
else lcp[i][j]=0;
}
}
}
bool cmp(int l,int r,int x,int y){ //判断s[l,r]是否<=s[x,y]
int len1=r-l+1,len2=y-x+1;
if(len1<len2) return true;
if(len1>len2) return false;
if(lcp[l][x]>=len1) return true;
return s[l+lcp[l][x]]<=s[x+lcp[l][x]];
}
int f[5005][5005],q[5005][5005];
int g[5005][5005],ming[5005][5005]; //最小段数,g数组的后缀最小值
void solve1(){
memset(g,0x3f,sizeof g);
memset(ming,0x3f,sizeof ming);
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
if(i!=j&&s[j]=='0'){
f[i][j]=0;
g[i][j]=inf;
}
else if(j==1){
f[i][j]=1;
g[i][j]=1;
}
else{
int k=j-1-(i-j+1)+1; k=max(k,1);
if(!cmp(k,j-1,j,i)) k++;
f[i][j]=((ll)(q[j-1][j-1]+mod-q[j-1][k-1]))%mod;
g[i][j]=ming[j-1][k]+1;
}
q[i][j]=((ll)(q[i][j-1]+f[i][j]))%mod;
}
for(int j=i;j>=1;j--) ming[i][j]=min(ming[i][j+1],g[i][j]);
}
printf("%d\n",q[n][n]);
}
void solve2(){
ll mi=1,val=0,ans=inf;
bool flag=false; //记录是否出现了解,如果 i 在 [n-16,n] 之内没有解要继续往前
for(int i=n;i>=max(1,n-16);i--){
val=val+(ll)(s[i]-'0')*mi; mi*=2ll;
if(f[n][i]!=0) flag=true,ans=min(ans,(ll)(g[n][i]+val));
}
if(flag){
printf("%lld\n",ans%mod);
return;
}
for(int i=n-17;i>=1;i--){
val=(val+(ll)(s[i]-'0')*mi)%mod; (mi*=2ll)%=mod;
if(f[n][i]!=0){
printf("%lld\n",(ll)(g[n][i]+val)%mod);
return;
}
}
}
signed main(){
cin>>s;
n=s.size(); s=' '+s;
Init();
solve1();
solve2();
return 0;
}
18.CF1562E Rescue Niwen!
sol1(人类智慧)
先 \(O(n^2)\) DP 求出所有 \(LCP(i,j)\)。
设 \(f[l,r]\) 表示以 \([l,r]\) 这个子串为末尾的 LIS , \(F[i]\) 表示 \(f[i,n]\)。
因为注意到对于 以 \([l,r]\) 这个子串为末尾的 LIS , 我一定可以往后面接 \([l,r+1] [l,r+2], ... ,[l,n]\) ,
所以最后的答案一定是 \(\max ^n_{i=1} f[i,n]\) ,即 \(\max ^n_{i=1} F[i]\)。
若 \(LCP(i,j)=len (j<i)\) :
- 如果 \(s[j+len]>s[i+len]\) ,则无法产生贡献
- 如果 \(s[j+len]<s[i+len]\) 则 \(F[i]=\max(F[i],F[j] + n - (i+len) +1)\) ,
就是说对于以 \([j,n]\) 这个子串为末尾的 LIS 再接上 \([i,i+len] , [i,i+len+1] , ... ,[i,n]\) 。
时间复杂度 \(O(n^2)\)
sol2(常规做法)
还有一种解法是根据 \(LCP\) 排序 (即按字典序排序),然后离散化,给每一个字符串赋一个代表他排名的值。
再按照题目要求排序,跑正常的 \(LIS\)。
时间复杂度\(O(n^2 \log n^2)\),看运气可过。
code(sol1)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T;
int n,lcp[5005][5005],f[5005];
string s;
signed main(){
T=read();
while(T--){
n=read();
cin>>s; s=' '+s;
for(int i=1;i<=n;i++){
f[i]=0;
for(int j=1;j<=n;j++){
lcp[i][j]=0;
}
}
lcp[n][n]=1;
for(int i=n;i>=1;i--){
for(int j=n;j>=1;j--){
if(i==n&&j==n) continue;
if(s[i]==s[j]) lcp[i][j]=lcp[i+1][j+1]+1;
else lcp[i][j]=0;
}
}
int ans;
ans=n;
for(int i=1;i<=n;i++){
f[i]=n-i+1;
for(int j=1;j<i;j++){
if(s[j+lcp[i][j]] < s[i+lcp[i][j]])
f[i]=max(f[i],f[j]+n-(i+lcp[i][j])+1);
}
ans=max(ans,f[i]);
}
printf("%d\n",ans);
}
return 0;
}
19.CF1701E Text Editor
若没有 home
操作,很明显不会用 right
, 此时答案为 \(n - LCP(S,T)\) ( LCP 为最长公共前缀)。
如果有 home
, 此时策略一定是这样的:
- 从后往前删,每一次花费 \(1\) 进行
left
或Backspace
- 花 \(1\) 按
home
- 从前往后删,每一次花费 \(1\) 按
right
或 花费 \(2\) 先按right
再按Backspace
- 剩余中间一段 \(S,T\) 是一样的不用动
考虑从前往后 和 从后往前 两次 DP , 以从前往后为例:
\(f[i][j]\) 表示从前往后操作 , 当前操作完光标在 \(i\) 后,让 \(S[1,i]\) 和 \(T[1,j]\) 匹配的最小代价。
转移:
- 删除 \(f[i][j]=min(f[i][j],f[i-1][j]+2)\)
- 不删 若 \(s[i]=t[j]\) , \(f[i][j]=min(f[i][j],f[i-1][j-1]+1)\)
从后往前类似,用 \(g[i][j]\) 表示 , 当前操作完光标在 \(j\) 前,让 \(S[i,n]\) 和 \(T[j,m]\) 匹配的最小代价
但是注意到我们还会有一段,即 剩余中间一段 \(S,T\) 是一样的不用动, 而在我们上面的转移中,这一段我们也会用移动建,但其实是不用的。
所以用 \(F[i][j]\) 表示从前往后让 \(S[1,i]\) 和 \(T[1,j]\) 匹配的最小操作次数
- 删除 \(F[i][j]=min(F[i][j],f[i-1][j]+2)\) , 因为要删除的话,一定是要把光标移到 \(i\) 之前的,所以转移用 \(f\)数组
- 不删 若\(s[i]=t[j]\) , \(F[i][j]=min(F[i][j],F[i-1][j-1])\) 可以不用移动。
\(G[i][j]\) 的定义和转移类似
最后的答案是:
最后一个是 home
操作的花费
这题卡空间,用 \(short\) 存储即可 (因为不能用滚动数组)。
code
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,inf=N;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T,n,m;
short f[N][N],g[N][N],F[N][N],G[N][N];
char s[N],t[N];
int Min(int x,int y){ //之所以手写是因为min不能比较 short 和 int
if(x>y) return y;
return x;
}
signed main(){
T=read();
while(T--){
n=read(),m=read();
scanf("%s%s",s+1,t+1);
for(int i=0;i<=n+1;i++){
for(int j=0;j<=m+1;j++){
F[i][j]=G[i][j]=f[i][j]=g[i][j]=inf;
}
}
F[0][0]=f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){ //注意把 j=0 循环上
F[i][j]=f[i][j]=f[i-1][j]+2;
if(s[i]==t[j]) f[i][j]=Min(f[i][j],f[i-1][j-1]+1),F[i][j]=Min(F[i][j],F[i-1][j-1]);
}
}
G[n+1][m+1]=g[n+1][m+1]=0;
for(int i=n;i>=1;i--){
for(int j=m+1;j>=1;j--){ //注意把 j=m+1 循环上
G[i][j]=g[i][j]=g[i+1][j]+1;
if(s[i]==t[j]) g[i][j]=Min(g[i][j],g[i+1][j+1]+1),G[i][j]=Min(G[i][j],G[i+1][j+1]);
}
}
int ans=inf;
for(int i=0;i<=n;i++){
for(int j=0;j<=m;j++){
ans=min(ans,F[i][j]+G[i+1][j+1]+(F[i][j]!=0));
}
}
if(ans==inf) printf("-1\n");
else printf("%d\n",ans);
}
return 0;
}
20.CF1954D Colored Balls
经典结论:一个集合的答案为 \(max(\lceil \frac {sum}{2} \rceil,maxn)\)
\(sum\) 为集合元素总个数,\(maxn\) 为出现次数最多的数的个数,除法取上整
背包求方案数再乘上答案即可。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+5,mod=998244353;
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
int n;
int a[N],f[N],g[N];
int calc(int x){
if(x&1) return x/2+1;
return x/2;
}
signed main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
sort(a+1,a+n+1);
int sum=0,ans=0;
f[0]=1;
for(int i=1;i<=n;i++){
sum+=a[i];
for(int j=N-1;j>=a[i];j--){
g[j]=f[j-a[i]];
(f[j]+=f[j-a[i]])%=mod;
}
for(int j=a[i];j<=sum;j++)
(ans+=g[j]*max(calc(j),a[i]))%=mod;
}
printf("%lld\n",ans);
return 0;
}
21.染色
题目大意:给你一个序列,每个位置有一个颜色,求把这个序列分成若干段,每段颜色数的平方之和的最小值。
暴力DP \(O(n^2)\): \(f[i]\) 表示把前 \(i\) 个位置染成对应颜色的最小值,转移时枚举 \(j\) , \(f[i]=min(f[i],f[j]+calc(j,i)^2)\) calc表示颜色数量
经典套路之---考虑DP值的范围:
注意到最终答案一定不大于n,因为我完全可以每一次只染色长度为 \(1\) 的区间
所以颜色数我们只用枚举到 \(\sqrt n\) 即可
具体来讲: 因为我们确定了颜色数之后,肯定一次染的长度越多越好,而颜色数我们只需要知道每个颜色的最后一个位置即可,所以我们只需要维护最后 \(\sqrt n\) 个颜色的最后一个位置,\(O(n \times \sqrt n)\)转移
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5,inf=5e5+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,a[N],f[N];
unordered_map<int,int> pos;
set<int> s;
signed main(){
// freopen("cyq.in","r",stdin);
// freopen("cyq.out","w",stdout);
while(scanf("%lld",&n)!=EOF){
for(int i=1;i<=n;i++) a[i]=read(),f[i]=inf;
int t=sqrt(n)+1+1;
f[0]=0; //注意不能写f[1]=1
s.clear();
s.insert(0);
for(int i=1;i<=n;i++){
if(!pos[a[i]]||(s.find(pos[a[i]])==s.end())){
pos[a[i]]=i,s.insert(i);
if(s.size()>t) s.erase(s.begin());
}
else{
s.erase(pos[a[i]]);
pos[a[i]]=i;
s.insert(pos[a[i]]);
}
int cnt=s.size()-1;
for(int x:s){
if(cnt==0) break;
f[i]=min(f[i],f[x]+cnt*cnt); //染色染[x+1,i]
cnt--;
}
}
printf("%lld\n",f[n]);
for(int i=1;i<=n;i++) pos.erase(a[i]);
}
return 0;
}
22.二进制翻转
对于操作序列:
\((x_1,y_1),(x_2,y_2),...,(x_k,y_k)\)
如果 \(x_i=x_j\) , 那我们可以把他们消掉,对于 y 也同理
假设消掉后剩下 \(a\) 个互不相同 \(x\),和 \(b\) 个互不相同的 \(y\),那么容易得到还剩 \(a\times m+b\times n-2\times a \times b\) 个 \(1\),我们完全可以暴力枚举这个 \(a\) 和 \(b\),我们只需要分开计算方案数即可。
设 \(f[i][j]\) 表示 构造长度为 \(i\) 的序列 \({x_1,x_2,...,x_i}\),按如上方法消掉之后剩余 \(j\) 个的方案数,转移时:
- 第 \(i\) 个 \(x\) 和前面的某个 \(x\) 抵消了,所以 \(f[i-1][j+1] \times (j+1) \to f[i][j]\)
- 第 \(i\) 个 \(x\) 和前面的任何一个 \(x\) 都不一样,所以 \(f[i-1][j-1] \times (n-j+1) \to f[i][j]\)
\(g[i][j]\)同理
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3005,mod=1e9+7;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,k,s;
int f[N][N],g[N][N];
signed main(){
n=read(),m=read(),k=read(),s=read();
f[0][0]=1;
for(int i=1;i<=k;i++){
for(int j=0;j<=n;j++){
f[i][j]=f[i-1][j+1]*(j+1)%mod;
if(j>0) (f[i][j]+=f[i-1][j-1]*(n-j+1)%mod)%=mod;
}
}
g[0][0]=1;
for(int i=1;i<=k;i++){
for(int j=0;j<=m;j++){
g[i][j]=g[i-1][j+1]*(j+1)%mod;
if(j>0) (g[i][j]+=g[i-1][j-1]*(m-j+1)%mod)%=mod;
}
}
int ans=0;
for(int a=0;a<=n;a++){
for(int b=0;b<=m;b++){
if(a*m+b*n-2ll*a*b!=s) continue;
if(a>k||b>k||((k-a)%2ll!=0)||((k-b)%2ll!=0)) continue;
(ans+=f[k][a]*g[k][b]%mod)%=mod;
}
}
printf("%lld\n",ans);
return 0;
}
23.不稳定的传送门
设 \(f[i]\) 表示 \(i\) 到 \(n\) 的最小期望花费,令 \(f[n]=n\)。
假设 \(i\) 的所有出边为 \((t_j,p_j,w_j) (1 \le j \le cnt_i)\),\(cnt_i\) 是 \(i\) 的出边数量。
我们按照一定顺序安排尝试顺序后则
我们适当换一下元,令 \(c_j=w_j+p_j \times f[t_j]\) (不是题目描述的 \(c\)),则:
要使它尽可能小,我们考虑邻项交换:
对于相邻两项 j,j+1,
原来的期望花费 \(= (1-p_1) \times (1-p_2)\times ...\times (1-p_{j-1})\times c_j + (1-p_1)\times (1-p_2)\times ...\times (1-p_{j-1})\times (1-p_j)\times c_{j+1}\)
交换之后的花费 = \(= (1-p_1) \times (1-p_2)\times ...\times (1-p_{j-1})\times c_{j+1} + (1-p_1)\times (1-p_2)\times ...\times (1-p_{j-1})\times (1-p_{j+1})\times c_j\)
当前一项比后一项小时,消去相同的项得到:
\(c_j+(1-p_j)\times c_{j+1} < c_{j+1}+(1-p_{j+1})\times c_j\)
按照这个写 cmp 即可
code
include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m;
struct P{
int t;
double p;
int w;
double c;
};
vector<P> G[N];
double f[N];
bool cmp(P x,P y){
return 1.0*x.c+(1.0-x.p)*y.c < 1.0*y.c+(1.0-y.p)*x.c;
}
signed main(){
n=read(),m=read();
for(int i=1;i<n;i++){
int w=read();
G[i].push_back({i+1,1.0,w,0});
}
for(int i=1;i<=m;i++){
int s=read(),t=read();
double p;
cin>>p;
int w=read();
G[s].push_back({t,p,w,0});
}
f[n]=0;
for(int u=n-1;u>=1;u--){
for(int i=0;i<G[u].size();i++)
G[u][i].c=G[u][i].w*1.0+G[u][i].p*f[G[u][i].t];
sort(G[u].begin(),G[u].end(),cmp);
double g=1;
for(P e:G[u]){
f[u]+=e.c*g;
g*=(1.0-e.p);
}
}
printf("%.2lf",f[1]);
return 0;
24.CF1444D Rectangular Polyline
这题其实没有什么动态规划,主要是构造,
考虑到这个是动态规划题单,并且题解有点复杂,所以具体见Booksnow 的题解
首先排除我懒。
只需要注意下涉及到的 bitset优化01背包
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int T;
int n,m,l[N],p[N],sumx,sumy;
vector<int> x[2],y[2];
int dx[N],dy[N],cnt;
bitset<N*N> f[N];
signed main(){
T=read();
f[0][0]=1; //不需要重复赋值
while(T--){
sumx=sumy=0;
x[0].clear(),x[1].clear();
y[0].clear(),y[1].clear();
n=read();
for(int i=1;i<=n;i++) l[i]=read(),sumx+=l[i];
m=read();
for(int i=1;i<=m;i++) p[i]=read(),sumy+=p[i];
if(n!=m||(sumx&1)||(sumy&1)){
puts("No");
continue;
}
for(int i=1;i<=n;i++) f[i]=f[i-1]|(f[i-1]<<l[i]);
if(!f[n][sumx/2]){
puts("No");
continue;
}
int lst=sumx/2;
for(int i=n;i>=1;i--)
if(lst>=l[i]&&f[i-1][lst-l[i]]) lst-=l[i],x[0].push_back(l[i]);
else x[1].push_back(l[i]);
for(int i=1;i<=m;i++) f[i]=f[i-1]|(f[i-1]<<p[i]);
if(!f[m][sumy/2]){
puts("No");
continue;
}
lst=sumy/2;
for(int i=m;i>=1;i--)
if(lst>=p[i]&&f[i-1][lst-p[i]]) lst-=p[i],y[0].push_back(p[i]);
else y[1].push_back(p[i]);
if(x[0].size()>y[0].size()) swap(x[0],x[1]);
if(x[0].size()>y[0].size()) swap(y[0],y[1]);
puts("Yes");
cnt=0;
for(int v:x[0]) dx[++cnt]=v;
for(int v:x[1]) dx[++cnt]=v;
cnt=0;
for(int v:y[0]) dy[++cnt]=v;
for(int v:y[1]) dy[++cnt]=v;
sort(dx+1,dx+x[0].size()+1,greater<int>());
sort(dx+x[0].size()+1,dx+y[0].size()+1,greater<int>());
sort(dx+y[0].size()+1,dx+n+1,greater<int>());
sort(dy+1,dy+x[0].size()+1);
sort(dy+x[0].size()+1,dy+y[0].size()+1);
sort(dy+y[0].size()+1,dy+m+1);
int X=0,Y=0;
for(int i=1;i<=x[0].size();i++){
X+=dx[i];printf("%d %d\n",X,Y);
Y+=dy[i];printf("%d %d\n",X,Y);
}
for(int i=x[0].size()+1;i<=y[0].size();i++){
X-=dx[i];printf("%d %d\n",X,Y);
Y+=dy[i];printf("%d %d\n",X,Y);
}
for(int i=y[0].size()+1;i<=n;i++){
X-=dx[i];printf("%d %d\n",X,Y);
Y-=dy[i];printf("%d %d\n",X,Y);
}
}
return 0;
}
25.CF1178F1 Short Colorful Strip
这是 F题的弱化版,保证了最终序列是个排列。
很明显两次染色操作要么包含要么相离,绝对不可能相交。
考虑区间DP: \(f[i][j]\) 表示染完区间 \([i,j]\) 的方案数
显然我们先染的一定是最小的那个颜色,假设那个颜色的位置为 \(k\)。
枚举第一次染色区间 \([x,y]\),显然 \([x,y]\) 包含 \(k\) ,
即 \(l \le x \le k \le y \le r\)。
因为后面就不能再染 \(k\) 这个位置了,且染色操作绝对不可能相交,所以区间: \([l,x-1],[x,k-1],[k+1,y],[y+1,r]\) 是独立的,把他们的方案数乘起来即可。
注意这里每个区间的先后顺序是唯一的(按照最小颜色排),所以不用乘 \(4!\)
即:
注意到这样是 \(O(n^4)\) ,所以把 \(x,y\) 拆开枚举即可:
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,a[505],f[505][505];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),m=read();
for(int i=1;i<=m;i++) a[i]=read();
for(int l=0;l<=m+1;l++){
for(int r=0;r<=m+1;r++){
f[l][r]=1;
}
}
for(int len=1;len<=m;len++){
for(int l=1;l+len-1<=m;l++){
int r=l+len-1;
if(len>1){
int sum1=0,sum2=0,ming=INT_MAX,pos;
for(int i=l;i<=r;i++)
if(a[i]<ming) ming=a[i],pos=i;
for(int x=l;x<=pos;x++) (sum1+=f[l][x-1]*f[x][pos-1])%=mod;
for(int y=pos;y<=r;y++) (sum2+=f[pos+1][y]*f[y+1][r])%=mod;
f[l][r]=sum1*sum2%mod;
}
}
}
printf("%lld\n",f[1][m]);
return 0;
}
26.CF1178F2 Long Colorful Strip
这题和上一题唯一的区别就是 : 这题纸带很长,每一种颜色不一定只有一种。
为了套用上一题的区间DP做法,考虑怎么缩小 \(m\)。
注意到,从一开始的一个颜色段,每一次操作最多增加 \(2\) 个颜色段,即,如果最终状态颜色段的数目\(>2\times n+1\),那肯定无解。
并且对于一个最终状态的颜色段,他们肯定是一起被染色的,否则后面就无法把他们一起染成目标颜色,于是可以把所有颜色段看成一个点,这样最多有 \(2 \times n+1\) 个点,由于 \(O(n^3)\) 肯定跑不满,所以可以借鉴上一题的做法。
因为每一种颜色不一定只有一种,即每一个区间 \([l,r]\) 不一定只有一个最小的颜色,所以我们枚举的 \([x,y]\) 要包含所有的 \(k_1,k_2,k_3,k_4,...k_{cnt}\)。
转移的式子为:
实现时还有一个细节要注意的是,比如样例中的: 2 1 2
虽然这一看就是 \(0\) ,但是程序会输出 \(4\)。
这是因为当我们根据 \(1\) 把它分成 \(2\) 和 \(2\) 两半后,这两半并不是独立的,要把它们染成 \(2\),必然要经过 \(1\)。
特判也很好处理:如果 \([l,r]\) 这段区间里的颜色中有颜色并没有全部出现则把它的DP值设为 \(0\)
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5,mod=998244353;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m;
int a[N],b[N],f[1005][1005],pre[1005][505];
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
n=read(),m=read();
for(int i=1;i<=m;i++){
b[i]=read();
}
int cnt=0;
for(int i=1;i<=m;i++){
int j;
for(j=i;b[j]==b[i];j++) ;
a[++cnt]=b[i],i=j-1;
}
m=cnt;
if(m>2*n+1){
printf("%d\n",0);
return 0;
}
for(int i=1;i<=m;i++){
pre[i][a[i]]=1;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
pre[i][j]+=pre[i-1][j];
}
}
for(int l=0;l<=m+1;l++){
for(int r=0;r<=m+1;r++){
f[l][r]=1;
}
}
for(int len=1;len<=m;len++){
for(int l=1;l+len-1<=m;l++){
int r=l+len-1;
for(int i=l;i<=r;i++){
int cnt1=pre[r][a[i]]-pre[l-1][a[i]],cnt2=pre[m][a[i]];
if(cnt1<cnt2){
f[l][r]=0;
break;
}
}
if(len>1){
int sum1=0,sum2=0,tmp=1,ming=INT_MAX;
for(int i=l;i<=r;i++)
if(a[i]<ming) ming=a[i];
int st=-1,ed=-1,lst=-1;
for(int i=l;i<=r;i++){
if(a[i]==ming){
if(lst!=-1)
(tmp*=f[lst+1][i-1])%=mod;
if(st==-1) st=i;
lst=ed=i;
}
}
for(int x=l;x<=st;x++) (sum1+=f[l][x-1]*f[x][st-1])%=mod;
for(int y=ed;y<=r;y++) (sum2+=f[ed+1][y]*f[y+1][r])%=mod;
f[l][r]*=sum1*sum2%mod*tmp%mod;
}
}
}
printf("%lld\n",f[1][m]);
return 0;
}
27.[CEOI2016] kangaroo
题目要求相当于是说只能来回横跳。
相当于要构造一个排列,这个排列满足:
对于每个连续的三个数,中间那个数是最大的或最小的,即整个序列呈波浪型。
关于这种序列满足一定形状的题,套路就是考虑从小到大插入每个位置。
设 \(f[i][j]\) 表示插入到 \(i\) , 一共有 \(j\) 个连续段的情况,接下来对于 \(i+1\) ,如果 \(i+1 \ne s或t\),有三种情况:
- 自成一段,即插在空隙里,一共有 \(j+1\) 个空隙,但如果此时已经插入了 \(s\)或\(t\) , 头或尾不能插
- 把两段接在一起,此时 \(i+1\) 一定大于其左右两个数(因为是从小到大),即他是波峰,一定满足题意,一共有 \(j-1\) 个选择
- 加在一段的开头或结尾 , 假设接在开头,那么此时 i+1 的右边比它要小,但左边由于还没放,到时候肯定比它要大,这样就会出现单调递减的三个数,就不符合题意了,所以不会出现这种情况。(当然如果 \(i+1=s或t\)时是可以的)
代码里用的是填表法,在 \(i=s或t\) 时要特判,此时只能放在开头/结尾。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e3+5,mod=1e9+7;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,s,t,f[N][N];
signed main(){
n=read(),s=read(),t=read();
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
if(i==s||i==t) f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
else f[i][j]=((j-(i>s)-(i>t))*f[i-1][j-1]%mod+j*f[i-1][j+1]%mod)%mod;
}
}
printf("%lld\n",f[n][1]);
return 0;
}
28.CF1312D Count the Arrays
套路和上题类似。
先假设我们是用给定的 \(n-1\) 个数来构造这个序列。
我们考虑从大到小考虑这 \(n-1\) 个数。
设 \(f[i][0/1]\) 表示构造长度为 \(i\) 的满足条件的序列, 并且 没有/有 出现两个相同的数,那对于
- \(f[i][0]\) :我可以把当前考虑的这个数放在序列的开始,也可以放在结尾 \(f[i][0]\gets 2 \times f[i-1][0]\)
- \(f[i][1]\) :这个数如果不是相同的那个数则 \(f[i][1]\gets2* \times[i-1][1]\),否则我就开头放一个,结尾放一个 \(f[i][1] \gets f[i-2][0]\)
我们只需要随便从 \(m\) 个数里选出 \(n-1\) 个数就好了,答案为 \(f[n][1] \times C_m^{n-1})\)
几个细节:
- \(i=1\)时,放在开头和结尾是一样的,不用\(\times2\)
- 只有 \(i \ge 3\)时才能进行 \(f[i][1] \gets f[i-2][0]\) 的转移,不然不满足严格单调
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5,mod=998244353;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,f[N][2];
int inv[N],fact[N],q[N];
int C(int n,int m){
if(n<m) return 0;
return fact[n]*q[m]%mod*q[n-m]%mod;
}
signed main(){
n=read(),m=read();
fact[0]=1;
for(int i=1;i<N;i++) fact[i]=fact[i-1]*i%mod;
inv[1]=1;
for(int i=2;i<N;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
q[0]=1;
for(int i=1;i<N;i++) q[i]=q[i-1]*inv[i]%mod;
f[0][0]=1;
for(int i=1;i<=n;i++){
if(i==1){
(f[i][0]=f[i-1][0])%=mod;
(f[i][1]=f[i-1][1])%=mod;
}
else{
(f[i][0]=2*f[i-1][0])%=mod;
(f[i][1]=2*f[i-1][1])%=mod;
}
if(i>=3) (f[i][1]+=f[i-2][0])%=mod;
}
printf("%lld\n",f[n][1]*C(m,n-1)%mod);
return 0;
}
29.CF1312E Array Shrinking
考虑区间DP,一段区间 \([l,r]\) 要么直接合成一个数,否则一定可以找到一个分界点 \(i\),使 \(f[l][r]=f[l][i]+f[i+1][r]\),分别进行DP即可。
code
#include<bits/stdc++.h>
using namespace std;
const int N=500+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,a[N];
int f[N][N]; //f[l][r]表示[l,r]剩余的最小长度
int g[N][N]; //g[l][r]表示区间[l,r]缩成一个数时的值,如果不能就=0
signed main(){
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int len=1;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
if(len==1) g[l][r]=a[l];
else if(len==2){
if(a[l]==a[r]) g[l][r]=a[l]+1;
}
else{
for(int i=l;i<=r;i++){
if(g[l][i]==g[i+1][r]&&g[l][i]!=0){
g[l][r]=g[l][i]+1;
break;
}
}
}
}
}
for(int len=1;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
f[l][r]=r-l+1;
if(g[l][r]) f[l][r]=1;
else{
for(int i=l;i<=r;i++){
f[l][r]=min(f[l][r],f[l][i]+f[i+1][r]);
}
}
}
}
printf("%d\n",f[1][n]);
return 0;
}
30.CF1312G Autocompletion
首先按照题目的意思可以建出一个Trie(这个建的方式有点奇怪,可以看代码)
假设 \(f[i]\) 表示打印出 \(i\) 号节点对应的字符串所需要的最小花费,\(id[i]\) 表示 \(i\) 号节点对应的字符串在 \(S\) 中的字典序。
做一个树形DP: \(f[i]=min(f[fa]+1,f[j]+id[i]-id[j])\), 其中 \(j\) 是 \(i\) 的祖先,即 \(j\) 表示的字符串是 \(i\) 的前缀。
考虑优化后面的转移,用一个栈来存储 \(f[i]-id[i]\),
在遍历到 \(i\) 的时候如果栈顶值比我大,那就入栈,回溯如果栈里面有就出栈,这样转移时直接取出栈顶即可,并且保证了栈里面的一定都是 i 的祖先
\(id\) 数组也不用真的建出来,只需要用变量维护即可,只不过那些不是 S 中的节点是不能算在里面的。
code
#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e6+5;
inline int read(){
int w = 1, s = 0;
char c = getchar();
for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
return s * w;
}
int n,m,id,a[N];
int ch[N][30],f[N];
bool stater[N];
stack<PII> st;
void dfs(int u){ //遍历到 u 的时候 f[u] 已经算出来了
if(!st.size()||(st.top().se>f[u]-id)) st.push({u,f[u]-id});
id+=stater[u]; //这里 id 其实表示的是比它字典序小的在 S 中的个数
for(int i=0;i<=25;i++){
if(!ch[u][i]) continue;
int v=ch[u][i];
f[v]=f[u]+1;
if(st.size()&&stater[v]) f[v]=min(f[v],st.top().se+id+1);
/*
首先自动补全的结果要是S中的字符串,所以要满足 stater[v]=true
其次,id[v]-id[u]在这里表示的是从u开始到v(包括u,不包括v)中字典序比 v 小的个数,但实际上是要算上 v 的,所以要 +1
*/
dfs(v);
}
if(st.size()&&st.top().fi==u) st.pop();
}
signed main(){
n=read();
for(int i=1;i<=n;i++){
int x=read();
char c;
cin>>c;
ch[x][c-'a']=i;
}
int m=read();
for(int i=1;i<=m;i++){
a[i]=read();
stater[a[i]]=true;
}
dfs(0);
for(int i=1;i<=m;i++) printf("%d ",f[a[i]]);
puts("");
return 0;
}