「动态规划 2022/02 」学习记录
Topcoder SRM 713 Medium DFS Count
给一个图,求不同的 DFS 序个数。
其中 \(n \le 18\) 。
设 \(f(S,i)\) 表示走过点集 \(S\) ,当前在 \(i\) 上。对于下一个点,必须知道之前可以回溯的点。
设 \(g(i,S)\) 表示从 \(i\) 出发经过 \(S\) 可以走到的点。
于是有转移,对于一个 \(j \in g(i,S)\) ,有 \(f(S \cup \{ j \},j) \leftarrow f(S \cup \{ j \},j) + f(S,i)\) 。
CF868E Policeman and a Tree
有一个 \(n\) 个点的树,有一个警察初始在 \(s\) 点,移动速度为 \(1\)。有 \(m\) 个小偷在 \(x_i\) 点,移动速度为无穷大。如果警察和小偷某个时刻在同一地点,小偷就会被抓住。
求最坏情况下警察抓住小偷需要的最少时间。
其中 \(n,m \le 50\) 。
首先本题一定有解。警察只有在把一个小偷怼到叶子节点的时候才会抓到。
考虑当前警察在 \(u\) 上,那么可以记录他周边点小偷的情况。但这个数量太大。
转为当警察在一条边 \((u,v)\) 上时的情况。设 \(f(e,x,y)\) 表示警察在 \(e\) 边上,\(v\) 子树内有 \(x\) 个小偷,子树外有 \(y\) 个。
如果 \(v\) 是叶子节点,则 \(f(e,x,y)=f(e',y,0)\) 。其中 \(e'\) 是 \(e\) 的反向边。
若不是叶子节点,那么在 \(v\) 上的小偷一定会跑到 \(v\) 连接的非 \(u\) 点。假设有 \(d\) 个。于是有:
于是考虑用类似背包的转移,设 \(g(i,j)\) 表示在 \(v\) 中的第 \(i\) 个子树放了 \(j\) 个罪犯的时间。
而 \(g\) 可以滚掉第一维,然后注意一下枚举时候的顺序,记忆化即可。复杂度 \(\mathcal{O}(n^5)\) 。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int N=50+5;
const int inf=1e9;
struct edge{
int v,w,nx;
}e[N<<1];
int n,m,ne=1,st,ans,f[N],d[N],sz[N],dp[N<<1][N][N];
void read(int u,int v,int w)
{ e[++ne].v=v;
e[ne].nx=f[u];
e[ne].w=w;
f[u]=ne;
d[u]++;
}
void dfs(int u,int ffa)
{ for(int i=f[u];i;i=e[i].nx)
{ int v=e[i].v;
if(v==ffa)continue;
dfs(v,u);
sz[u]+=sz[v];
}
}
int DP(int et,int x,int y)
{ if(!x&&!y)return 0;
if(dp[et][x][y]!=-1)return dp[et][x][y];
int u=e[et].v;
if(d[u]==1)return (y==0)?(0):(DP(et^1,y,0)+e[et].w);
int g[N];
memset(g,0,sizeof(g));
g[0]=inf;
for(int i=f[u];i;i=e[i].nx)
{ if(i==(et^1))continue;
for(int j=x;j>=1;j--)
for(int k=j;k>=1;k--)
g[j]=max(g[j],min(g[j-k],DP(i,k,x+y-k)+e[i].w));
}
return dp[et][x][y]=g[x];
}
int main()
{ scanf("%d",&n);
for(int i=1,u,v,w;i<n;i++)
{ scanf("%d%d%d",&u,&v,&w);
read(u,v,w),read(v,u,w);
}
scanf("%d%d",&st,&m);
for(int i=1,x;i<=m;i++)
{ scanf("%d",&x);
sz[x]++;
}
dfs(st,0);
memset(dp,-1,sizeof(dp));
ans=inf;
for(int i=f[st];i;i=e[i].nx)
{ int v=e[i].v;
ans=min(ans,DP(i,sz[v],m-sz[v])+e[i].w);
}
printf("%d\n",ans);
return 0;
}
CF762F Tree nesting 的题解。
给定两棵树 \(S,T\),求 \(S\) 有多少个连通子图与 \(T\) 同构。
其中 \(|S| \le 10^3,|T| \le 12\) 。
首先因为 \(T\) 很小,所以可以对每个点作根的情况状压。每次状压每个点连接的儿子即可。
那么对于 \(S\) 就不用枚举根,所以只要考虑每个子树即可。
记忆化搜索,每次搜索设 \(f(u,v,i)\) 表示在 \(u\) 子树内,匹配到第 \(v\) 个儿子的子树,匹配的状态是 \(i\) 的方案数。于是有:
其中 \(j\) 是点 \(k\) 的状压。
注意可能出现自己和自己配对的情况,所以最后的答案还要除以 \(T\) 和 \(T\) 同构匹配的情况。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=1000+5,M=1<<12;
const int Mod=1e9+7;
struct edge{
int v,nx;
};
struct tree{
int n,ne,f[N],sz[N],bit[N],son[N][N];
edge e[N<<1];
void read(int u,int v)
{ e[++ne].v=v;
e[ne].nx=f[u];
f[u]=ne;
}
}S,T;
int ans,ans2,dp[N][M];
bool vis[N][M];
int qpow(int x,int k)
{ int res=1;
while(k)
{ if(k&1)res=1ll*res*x%Mod;
x=1ll*x*x%Mod;k>>=1;
}
return res;
}
void dfs1(int u,int ffa)
{ S.sz[u]=S.bit[u]=0;
for(int i=S.f[u];i;i=S.e[i].nx)
{ int v=S.e[i].v;
if(v==ffa)continue;
S.son[u][++S.sz[u]]=v;
dfs1(v,u);
}
}
void dfs2(int u,int ffa)
{ T.sz[u]=T.bit[u]=0;
for(int i=T.f[u];i;i=T.e[i].nx)
{ int v=T.e[i].v;
if(v==ffa)continue;
T.son[u][++T.sz[u]]=v;
dfs2(v,u);
T.bit[u]|=(1<<(v-1));
}
}
int solve(int u,int v,int A)
{ if(!v)return (!A);
int t=S.son[u][v];
if(vis[t][A])return dp[t][A];
vis[t][A]=1;
dp[t][A]=solve(u,v-1,A);
for(int i=0;i<T.n;i++)
if((A>>i)&1)dp[t][A]=(dp[t][A]+1ll*solve(u,v-1,A^(1<<i))*solve(t,S.sz[t],T.bit[i+1])%Mod)%Mod;
return dp[t][A];
}
int main()
{ scanf("%d",&S.n);
for(int i=1,u,v;i<S.n;i++)
{ scanf("%d%d",&u,&v);
S.read(u,v),S.read(v,u);
}
scanf("%d",&T.n);
for(int i=1,u,v;i<T.n;i++)
{ scanf("%d%d",&u,&v);
T.read(u,v),T.read(v,u);
}
dfs1(1,0);
for(int i=1;i<=T.n;i++)
{ memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
dfs2(i,0);
for(int j=1;j<=S.n;j++)ans=(ans+solve(j,S.sz[j],T.bit[i]))%Mod;
}
S=T;
dfs1(1,0);
for(int i=1;i<=T.n;i++)
{ memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
dfs2(i,0);
ans2=(ans2+solve(1,S.sz[1],T.bit[i]))%Mod;
}
printf("%lld\n",1ll*ans*qpow(ans2,Mod-2)%Mod);
return 0;
}
LOJ2292 成绩单
因为不知道为什么题解还没写(?)。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=50+5;
int n,A,B,a[N],b[N],g[N][N],f[N][N][N][N];
int main()
{ scanf("%d%d%d",&n,&A,&B);
for(int i=1;i<=n;i++)
{ scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+1+n);
int m=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+1+m,a[i])-b;
memset(f,0x3f,sizeof(f));memset(g,0x3f,sizeof(g));
for(int i=1;i<=n;i++)f[i][i][a[i]][a[i]]=0,g[i][i]=A;
for(int k=1;k<=n;k++)
{ for(int i=1;i+k-1<=n;i++)
{ int j=i+k-1;
for(int x=1;x<=m;x++)
for(int y=x;y<=m;y++)
{ f[i][j][min(x,a[j])][max(y,a[j])]=min(f[i][j][min(x,a[j])][max(y,a[j])],f[i][j-1][x][y]);
for(int k=i;k<j;k++)f[i][j][x][y]=min(f[i][j][x][y],f[i][k][x][y]+g[k+1][j]);
}
for(int x=1;x<=m;x++)
for(int y=x;y<=m;y++)
g[i][j]=min(g[i][j],f[i][j][x][y]+A+B*(b[y]-b[x])*(b[y]-b[x]));
}
}
printf("%d\n",g[1][n]);
return 0;
}
LOJ 6240 仙人掌
求对一个仙人掌随机点分治复杂度的期望。
其中 \(n \le 400\) 。
设 \(p(i,j)\) 表示对于点 \(i\),在 \(j\) 删除前,\(i,j\) 连通的概率。
那么答案可以表示为 \(\sum\limits_{i=1}^n \sum\limits_{j=1}^n p(i,j)\) 。
如果给定的是一棵树,那么有 \(p(i,j)=\dfrac{1}{dis(i,j)}\) 。
那如果有环,那先建一个圆方树。
那么对于圆点,一定不能先选,对于环,一定会有 2 个路径,不能出现两个路径上的点同时被先选。
设 \(i \rightarrow j\) 上走过的环上的点有 \(cnt\) 个,设 \(dp_x\) 表示大小为 \(x\) 满足删除这些点仍满足 \(i,j\) 联合的点集。选 \(j\) 前已经选了 \(k\) 个点。于是有:
于是可以直接 DP 了。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
const ll N=800+5;
const ll Mod=998244353;
struct edge{
ll v,nx;
}e[N<<2],e2[N<<2];
ll n,m,ne,ne2,tot,top,f[N],f2[N],dfn[N],low[N],st[N];
ll ans,ex,sz[N],inv[N],C[N][N],ic[N][N],rk[N][N],dp[N][N];
ll qpow(ll x,ll k)
{ ll res=1;
while(k)
{ if(k&1)res=res*x%Mod;
x=x*x%Mod;
k>>=1;
}
return res;
}
void read(ll u,ll v)
{ e[++ne].v=v;
e[ne].nx=f[u];
f[u]=ne;
}
void read2(ll u,ll v)
{ e2[++ne2].v=v;
e2[ne2].nx=f2[u];
f2[u]=ne2;
}
void dfs(ll u)
{ dfn[u]=low[u]=++tot;
st[++top]=u;
for(ll i=f[u];i;i=e[i].nx)
{ ll v=e[i].v;
if(!dfn[v])
{ dfs(v);
low[u]=min(low[u],low[v]);
if(dfn[u]==low[v])
{ ll tmp=0;ex++;
while(tmp!=v)
{ tmp=st[top--];
read2(ex+n,tmp),read2(tmp,ex+n);
rk[ex][tmp]=++sz[ex];
}
read2(ex+n,u),read2(u,ex+n);
rk[ex][u]=++sz[ex];
}
}
else low[u]=min(low[u],dfn[v]);
}
}
void solve(ll u,ll ffa,ll cnt)
{ if(u<=n)
{ for(ll i=0;i<=cnt;i++)ans=(ans+dp[u][i]*ic[cnt+1][i]%Mod*inv[cnt+1-i]%Mod)%Mod;
for(ll i=f2[u];i;i=e2[i].nx)
{ ll v=e2[i].v;
if(v==ffa)continue;
solve(v,u,cnt);
}
return;
}
static ll tmp[N][N];
memcpy(tmp[1],dp[ffa],sizeof(dp[ffa]));
for(ll i=2;i<=sz[u-n];i++)
{ memset(tmp[i],0,sizeof(tmp[i]));
for(ll j=0;j<=cnt+i;j++)
{ tmp[i][j]=tmp[i-1][j];
if(j)tmp[i][j]=(tmp[i][j]+tmp[i-1][j-1])%Mod;
}
}
for(ll i=f2[u];i;i=e2[i].nx)
{ ll v=e2[i].v;
if(v==ffa)continue;
ll dis=abs(rk[u-n][ffa]-rk[u-n][v]);
for(ll j=0;j<cnt+sz[u-n];j++)
dp[v][j]=(tmp[dis][j]+tmp[sz[u-n]-dis][j]-dp[ffa][j]+Mod)%Mod;
}
for(ll i=f2[u];i;i=e2[i].nx)
{ ll v=e2[i].v;
if(v==ffa)continue;
solve(v,u,cnt+sz[u-n]-1);
}
}
int main()
{ scanf("%lld%lld",&n,&m);
for(ll i=0;i<=n;i++)
{ inv[i]=qpow(i,Mod-2);
C[i][0]=ic[i][0]=1;
for(ll j=1;j<=i;j++)
{ C[i][j]=(C[i-1][j]+C[i-1][j-1])%Mod;
ic[i][j]=qpow(C[i][j],Mod-2);
}
}
for(ll i=1,u,v;i<=m;i++)
{ scanf("%lld%lld",&u,&v);
read(u,v);read(v,u);
}
dfs(1);
for(ll i=1;i<=n;i++)
{ memset(dp,0,sizeof(dp));
dp[i][0]=1;
solve(i,0,0);
}
printf("%lld\n",ans);
return 0;
}
[AGC016F] Games on DAG
给定一个 \(n\) 个点 \(m\) 条边的 DAG,对于每条边 \((u,v)\) 都满足 \(u<v\),\(1,2\) 号点各一个石头。
每次可以沿DAG上的边移动一颗石头,不能移动则输,求所有 \(2^m\) 个边的子集中,只保留这个子集先手必胜的方案个数。
其中 \(2 \le n \le 15,1 \le m \le \dfrac{n(n-1)}{2}\) 。
首先可以发现,先手必胜的情况是 \(\operatorname{sg}(1) \neq \operatorname{sg}(2)\) 。所以计算 \(2^m\) 减去两个相等的情况即可。
接下来我们考虑对于所有的 \(\operatorname{sg}(i)=x\) 的连边情况。
首先,对于所有的 \(y<x\) ,必须至少向一个 \(\operatorname{sg}(j)=y\) 连边。
其次,他们之间不能相互连边。
最后,对于所有 \(y>x\) ,对于这个 \(\operatorname{sg}(j)=y\) 连也可以不连也可以。
于是考虑从 \(\operatorname{sg}(i)=0\) 开始考虑。同样的,对于所有为 \(0\) 的点,他们之间不能同时连边,剩下的点他们可以随意连。
也就是说,对于一张图,我们可以先把所有 \(\operatorname{sg}(i)=0\) 的点全部导出来,然后再计算剩下的图。而对于剩下的图,我们把所有 \(\operatorname{sg}(i)\) 减去 \(1\) ,于是也变成了导出为 \(0\) 的点。
于是设 \(f(s)\) 表示仅考虑 \(s\) 中的点来导出子图,满足 \(\operatorname{sg}(1)=\operatorname{sg}(2)\) 的连边方案数。此时的 \(1,2 \in s\) 。
转移时候枚举 \(s\) 的子集 \(t\) 。转移过来就可以了。注意 \(t\) 中也要满足 \(\operatorname{sg}(1)=\operatorname{sg}(2)\) 。
分类一下 \(1,2 \in s\) 和 \(1,2 \notin s\) 的情况。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=15;
const int Mod=1e9+7;
int n,m,pw[N*N],e[N][N];
int g[1<<N][N],f[1<<N];
int main()
{ scanf("%d%d",&n,&m);
for(int i=1,u,v;i<=m;i++)
{ scanf("%d%d",&u,&v);
e[u-1][v-1]=1;
}
pw[0]=1;
for(int i=1;i<=m;i++)pw[i]=pw[i-1]*2%Mod;
for(int s=1;s<(1<<n);s++)
{ int j=0;
while(~s>>j&1)j++;
for(int i=0;i<n;i++)g[s][i]=g[s^(1<<j)][i]+e[i][j];
}
for(int s=0;s<(1<<n);s++)
{ if((s&3)!=3)continue;
f[s]=1;
for(int t=s&(s-1);t>=1;t=(t-1)&s)
{ if((t&1)!=((t>>1)&1))continue;
if(t&1)
{ int res=1;
for(int i=0;i<n;i++)
if((s>>i)&1)
{ if((t>>i)&1)res=1ll*res*(pw[g[s^t][i]]-1)%Mod;
else res=1ll*res*pw[g[t][i]]%Mod;
}
f[s]=(f[s]+1ll*res*f[t]%Mod)%Mod;
}
else{
int res=1;
for(int i=0;i<n;i++)
if((s>>i)&1)
{ if((t>>i)&1)res=1ll*res*(pw[g[s^t][i]]-1)%Mod*pw[g[t][i]]%Mod;
else res=1ll*res*pw[g[t][i]]%Mod;
}
f[s]=(f[s]+res)%Mod;
}
}
}
printf("%d\n",(pw[m]-f[(1<<n)-1]+Mod)%Mod);
return 0;
}
300iq Contest 3 H Horrible Cycles
给一个 \(2n\) 个点的二分图,左边第 \(i\) 个点向右边前 \(a_i\) 个点连边,求图中的简单环个数。
其中 \(n \le 5000\) 。
考虑对 \(2n\) 个点排序,使得对于每个左部点都会和前面的右部点连边。考虑每次先编号右部点然后删掉,如果出现一个左部点没有连边那么再给这个左部点标号。
考虑把环拆成链来计算,因为后面的点一定会连到前面的点。设 \(f(i,j)\) 表示对于前 \(i\) 个点,形成 \(j\) 条链的方案数。初始时 \(f(0,0)=1\) 。
如果 \(i\) 是右部点,由于排序所以他和前面都没有连边,所以他可以自己成链。
如果 \(i\) 是左部点,那么他和前面所有的右部点都有连边,所以他会把之前的两条合并到一条。
最后答案要减去二元环的情况,减去所有边即可。然后将答案除以 \(2\) ,因为考虑链的时候是考虑顺序的。
Atcoder CF2017ETR3 F Unicyclic Graph Counting
求 \(n\) 个点的基环树个数,满足第 \(i\) 个点的度数为 \(d_i\)。
其中 \(n \le 300\) 。
首先,如果不看环生成 prufer
序列,那么最后一定会剩下一个环和环旁边的一个点。
对于一个普通的树,如果第 \(i\) 个点度数是 \(d_i\) ,那么答案为 \(\dfrac{(n-2)!}{\prod{(d_i-1)!}}\) 。那么对于基环树,在树上的点就是 \(d_i-1\) 的 出现次数,环上的就是 \(d_i-2\) ,环上和环外的特殊点是 \(d_i-3\) 。
于是有答案 \(\dfrac{(n-|S|-1)!}{\prod(d_i-k)!}\) 。其中 \(k\) 是决定点的类型,\(S\) 是环的集合。
那我们考虑枚举环,但这样显然会 T 。因为同样大小的环本质相同,所以变成枚举环的大小。
于是变成要计算对于一个大小所有的环的 \(\prod(d_i-k)\) 贡献。
这部分考虑 DP ,设 \(f(i,j,k)\) 表示考虑前 \(i\) 个点,有 \(j\) 个在环上,已经有了 \(k=0/1\) 个特殊点。分类直接转移即可。
注意这个 DP 可以用分治 FFT 优化,但原题不需要我没写(?
以及因为 prufer
序列不考虑一个点的情况,所以需要特判只有一个大环的情况。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
const ll N=300+5;
const ll Mod=1e9+7;
ll n,ans,d[N],mul[N],inv[N],fmul[N],f[N][N][2];
int main()
{ scanf("%lld",&n);
for(ll i=1;i<=n;i++)
scanf("%lld",&d[i]);
mul[0]=mul[1]=inv[0]=inv[1]=fmul[0]=fmul[1]=1;
for(ll i=2;i<=n;i++)
{ inv[i]=(Mod-Mod/i)*inv[Mod%i]%Mod;
mul[i]=mul[i-1]*i%Mod,fmul[i]=fmul[i-1]*inv[i]%Mod;
}
bool is=1;
for(int i=1;i<=n;i++)
if(d[i]!=2)is=0;
if(is==1)
{ printf("%lld\n",mul[n-1]*inv[2]%Mod);
return 0;
}
f[0][0][0]=1;
for(ll i=1;i<=n;i++)
for(ll j=0;j<=i;j++)
for(ll k=0;k<2;k++)
{ if(d[i]>=1)f[i][j][k]=(f[i][j][k]+f[i-1][j][k]*fmul[d[i]-1]%Mod)%Mod;
if(d[i]>=2)f[i][j][k]=(f[i][j][k]+f[i-1][j-1][k]*fmul[d[i]-2]%Mod)%Mod;
if(d[i]>=3&&k)f[i][j][1]=(f[i][j][1]+f[i-1][j-1][0]*fmul[d[i]-3]%Mod)%Mod;
}
for(ll i=3;i<=n;i++)
ans=(ans+f[n][i][1]*mul[n-i-1]%Mod*mul[i-1]%Mod*inv[2]%Mod)%Mod;
printf("%lld\n",ans);
return 0;
}
300iq Contest 1 J Jealous Split
给一个长度为 \(n\) 的序列,将其划分为 \(k\) 部分,记 \(s_i\) 表示第 \(i\) 部分的和,\(m_i\) 表示第 \(i\) 部分的最大值,构造这样一个划分使得:
\[\forall 1 \le i <k,|s_i-s_{i+1}| \le \max(m_i,m_{i+1}) \]其中 \(n \le 10^5\) 。
最后只需要构造,所以考虑一种不合法的情况怎么变成合法。
可以通过调整分界点,在相邻两块最大值不变的情况下,通过这个调整,只会出现和的差变小。那可以考虑维护每段的和的平方的和。
于是题目变成最小化每段和的平方的和,答案是关于 \(k\) 的凸函数,于是可以 wqs 二分。
Yandex Algorithm 2018 Final Round D LIS vs. LDS
给定一个排列,求一组不相交的 LIS 和LDS 。
其中 \(n \le 5 \times 10^5\)。
性质题。首先要发现一个性质,LIS 和 LDS 至多有一个交点。
不妨记 \(f_i\) 表示经过 \(i\) 的 LDS 个数,即从前往后跑 LDS 从后之前跑 LIS 的方案积,再记一个 \(S\) 表示 LDS 总和。
考虑一个 LIS 什么时候一定不合法,也就是不存在一个 LDS 和他没有交点。也就是对于一组 LIS \(\{ i_1,i_2,...,i_k \}\) ,满足其 \(\sum\limits_{j=1}^k f_{i_j}=S\) ,那么一定不合法。
所以只要找一个不满足这个条件的 LIS 即可。
不妨找两个 LIS ,使他们的 \(f\) 和不同,这样最多有一个不合法。
因为这个数字可能很大,所以考虑对一个大质数取膜来优化代码部分(
CF868F Yet Another Minimization Problem
将一个长度为 \(n\) 的序列分为 \(k\) 段,一段 \([l,r]\) 的代价是 \(l \le i < j \le r,a_i=a_j\) 的 \((i,j)\) 对数。最小化分割代价。
其中 \(n \le 10^5,k \le 20\) 。
先考虑暴力 DP ,设 \(f(i,j)\) 表示前 \(i\) 个分了 \(j\) 段的情况,设 \(\operatorname{calc}(k,i)\) 表示区间 \([k,i]\) 的贡献。于是显然有:
暴力 DP 过不去就是决策单调。
首先 \(\operatorname{calc}()\) 是单调的。
考虑决策点 \(x,y\) ,其中 \(x<y\) 。那么对于 \(i\) 右移前有 \(f(x-1,j-1)+\operatorname{calc}(x,i) > f(y-1,j-1)+\operatorname{calc}(y,i)\) 。那么右移后依然有。
于是决策单调。
因此我们考虑先枚举 \(k\) ,于是可以分治计算。计算每段的贡献类似用莫队的方法计算。 复杂度 \(\mathcal{O}(nk \log n)\) 。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
typedef long long ll;
const ll N=2e5+5;
const ll inf=1e15;
ll n,m,sum,l,r,a[N],cnt[N],dp[N][25];
ll calc(ll L,ll R)
{ while(l>L)sum+=cnt[a[--l]]++;
while(r<R)sum+=cnt[a[++r]]++;
while(l<L)sum-=--cnt[a[l++]];
while(r>R)sum-=--cnt[a[r--]];
return sum;
}
void solve(ll L,ll R,ll x,ll y,ll dep)
{ ll mid=(L+R)>>1,pos,tl=max(1ll,x),tr=min(mid,y);
ll mn=inf;
for(ll i=tl;i<=tr;i++)
{ ll val=dp[i-1][dep-1]+calc(i,mid);
if(val<mn)mn=val,pos=i;
}
dp[mid][dep]=mn;
if(L==R)return;
solve(L,mid,x,pos,dep),solve(mid+1,R,pos,y,dep);
}
int main()
{ scanf("%lld%lld",&n,&m);
for(ll i=1;i<=n;i++)dp[i][0]=inf;
for(ll i=1;i<=n;i++)
scanf("%lld",&a[i]);
cnt[0]=1;
for(ll i=1;i<=m;i++)solve(1,n,1,n,i);
printf("%lld\n",dp[n][m]);
return 0;
}