dp杂题选做
树的数量
题目其实挺简单的,难点在于状态的设计(其实也没多难)。
令 \(f_i\) 表示 \(i\) 个点的 \(m\) 叉树的数量,发现无法转移。设 \(g_{i,j}\) 表示根节点所在子树内有 \(i\) 个节点,\(j\) 个儿子(儿子所在子树可以为空)。可以写出转移方程:\(g_{i,j}=\sum\limits_{k=0}^{i-1} g_{i-k,j-1}\times f_k\),\(f_i=g_{i,m}\)
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=200,mod=1e4+7;
int n,m,f[N][N];
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
read(n),read(m);
for(int i=0;i<=m;i++){
f[1][i]=f[0][i]=1;
}
for(int i=2;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=0;k<i;k++){
f[i][j]=(f[i][j]+f[i-k][j-1]*f[k][m]%mod)%mod;
}
}
}
write_endl(f[n][m]);
return 0;
}
[SCOI2015]小凸玩密室
个人觉得这题非常神秘。
根据题目可知每次访问操作是访问完自己的子树,再访问父亲。所以除了第一次任意选点,剩下的都是从某个叶子节点跳到它的一个祖先或者往它的某棵子树跳。显然一颗子树对于后面的影响在于遍历完这颗子树后停在了哪里。
令 \(f_{u,x}\) 表示从根开始,访问完以 \(u\) 为根的子树后停在 \(x\) 处的最小费用,\(ls/rs\) 表示左/右儿子。分类讨论可以得到长得极为对称的两个转移方程
当 \(x\) 在左子树,\(f_{u,y}=\min\{dis(u,ls)\times a_{ls}+f_{ls,x}+dis(x,rs)\times a_{rs}+f_{rs,y}\}\)
当 \(x\) 在右子树,\(f_{u,y}=\min\{dis(u,rs)\times a_{rs}+f_{rs,x}+dis(x,ls)\times a_{ls}+f_{ls,y}\}\)
这个方程的复杂度接近 \(O(n^2)\),显然不能接受。
拆开式子,\(dis(x,rs)=dis(u,rs)+dis(x,u)\),发现只有 \(dis(x,u)\) 是个影响答案的关键信息,将它记录下来。
但因为没有要求从 \(1\) 开始,所以定义另一个状态 \(g_{u,x}\) 表示访问完以 \(u\) 为根的子树后停在 \(x\) 处的最小费用。继续分类讨论
当 \(x\) 在左子树,\(g_{u,y}=\min\{f_{u,y},f_{ls,x}+dis(x,u)\times a_u+dis(rs,u)\times a_{rs}+f_{rs,y}\}\)
当 \(x\) 在右子树,\(g_{u,y}=\min\{f_{u,y},f_{rs,x}+dis(x,u)\times a_u+dis(ls,u)\times a_{ls}+f_{ls,y}\}\),最后从所有 \(g_{1,x}\) 中取最小值即可。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=2e5+10,inf=1e18;
int dis[N],n,a[N],b[N];
vector<int>d[N],f[N],g[N];
int ls(int p){
return p<<1;
}
int rs(int p){
return p<<1|1;
}
int fa(int p){
return p>>1;
}
void dfs(int u){
dis[u]=dis[fa(u)]+b[u];
if(ls(u)<=n){
dfs(ls(u));
int s=f[ls(u)].size();
if(rs(u)<=n){
dfs(rs(u));
int ans1=inf,ans2=inf,ansp1=inf,ansp2=inf;
for(int i=0;i<f[u].size();i++){
if(i<s){
ans1=min(ans1,f[ls(u)][i]+b[ls(u)]*a[ls(u)]+(d[u][i]+b[rs(u)])*a[rs(u)]);
ansp1=min(ansp1,g[ls(u)][i]+d[u][i]*a[u]+b[rs(u)]*a[rs(u)]);
}
else{
ans2=min(ans2,f[rs(u)][i-s]+b[rs(u)]*a[rs(u)]+(d[u][i]+b[ls(u)])*a[ls(u)]);
ansp2=min(ansp2,g[rs(u)][i-s]+d[u][i]*a[u]+b[ls(u)]*a[ls(u)]);
}
}
for(int i=0;i<f[u].size();i++){
if(i<s){
f[u][i]=ans2+f[ls(u)][i];
g[u][i]=min(f[u][i],ansp2+f[ls(u)][i]);
}
else{
f[u][i]=ans1+f[rs(u)][i-s];
g[u][i]=min(f[u][i],ansp1+f[rs(u)][i-s]);
}
}
}
else{
for(int i=u;i>=1;i/=2){
f[i].pb(0);
g[i].pb(0);
d[i].pb(dis[u]-dis[i]);
}
f[u][0]=b[ls(u)]*a[ls(u)];
g[u][0]=inf;
f[u][1]=inf;
g[u][1]=b[ls(u)]*a[u];
}
}
else{
for(int i=u;i>=1;i/=2){
f[i].pb(0);
g[i].pb(0);
d[i].pb(dis[u]-dis[i]);
}
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
read(n);
for(int i=1;i<=n;i++){
read(a[i]);
}
for(int i=2;i<=n;i++){
read(b[i]);
}
dfs(1);
int ans=inf;
for(int i=0;i<g[1].size();i++){
ans=min(ans,g[1][i]);
}
write_endl(ans);
return 0;
}
[SHOI2005]树的双中心
这题一开始最让人感到神秘的是它限制了树高 \(h\),这个东西一定是有用的,虽然我们仍未知道它有什么用。
回归正题,\(\min(d_{u,x},d_{u,y})\) 告诉我们它其实是在路径 \((x,y)\) 上断了一条边,使其变成两棵树。因为若存在一个点 \(u\),使得 \(dis_{u,x}<d_{u,y}\)。那么与 \(u\) 相连且不在路径 \((x,y)\) 上的点 \(v\) 到 \(x\) 或 \(y\) 必然会经过 \(u\)。因为 \(dis_{u,x}<d_{u,y}\),所以 \(dis_{u,x}+dis_{v,u}<dis_{v,u}+dis_{u,y}\)。因此我们证明了它本质上相当于在路径 \((x,y)\) 上断一条边,使其变成两棵树,求所有这样的权值之和的最小值。
这个复杂度有点大,考虑转化一下。考虑断哪条边,对于断出来的两颗树,就变成了医院设置这道题,这两棵树的带权重心是断这条边的答案取值最佳点。虽然分成的两棵树上的点最后不一定会分别去两个带权重心,但调整后删去的边改变,且答案并不劣于这样的分配,所以这样分配后对答案无影响。
考虑枚举断边后,如何求得这两个带权重心的贡献。令 \(f_i\) 表示整棵树上的点到 \(i\) 的距离乘上点权之和。先令 \(1\) 号点为根,求出每个点 \(u\) 的带权子树大小 \(siz_u\) 和在 \(u\) 子树内的 \(f_u\) 的值。已知每棵子树的根节点 \(root\) 的 \(f\) 权值 \(f_root\),求整棵树的 \(f\) 权值,考虑换根。对于 \(v\in son_u,f_v=f_u+siz_{root}-siz_v-siz_v\)。若将根从 \(u\) 换到 \(v\),\(v\) 子树外的点 \(x\) 会额外造成 \(val_x\) 的贡献,\(v\) 子树内的点 \(x\) 会减少 \(val_x\) 的贡献。但显然我们没必要枚举每一个儿子,显然只有最大的 \(siz_v\) 且 \(siz_v\times 2<siz_{root}\) 时,才会使答案减小,这个 \(v\) 在原树上显然是带权重儿子或带权次重儿子。有可能是带权次重儿子的原因是去掉割掉的子树后,带权重儿子可能不是剩下那棵树中的带权重儿子。
可以发现这样一次换根的复杂度最多为 \(O(h)\),加上枚举断边,总复杂度为 \(O(nh)\)。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=5e4+10,inf=1e12;
int n,f[N],dep[N],siz[N],val[N],fa[N];
int heavy[N],nxt[N];
vector<int>e[N];
void dfs(int u,int father){
siz[u]=val[u];
fa[u]=father;
for(auto v:e[u]){
if(v==father){
continue;
}
dep[v]=dep[u]+1;
dfs(v,u);
siz[u]+=siz[v];
f[u]+=f[v]+siz[v];
if(siz[v]>siz[heavy[u]]){
nxt[u]=heavy[u];
heavy[u]=v;
}
else if(siz[v]>siz[nxt[u]]){
nxt[u]=v;
}
}
}
int Sum,pos;
void get_ans(int u,int res,int mx){
Sum=min(Sum,res);
int p=heavy[u];
if(p==pos||siz[p]<siz[nxt[u]]){
p=nxt[u];
}
if(!p){
return;
}
if(mx<siz[p]*2){
get_ans(p,res+mx-siz[p]*2,mx);
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
read(n);
for(int i=1;i<n;i++){
int u,v;
read(u),read(v);
e[u].pb(v);
e[v].pb(u);
}
for(int i=1;i<=n;i++){
read(val[i]);
}
dfs(1,0);
int ans=inf;
for(int i=2;i<=n;i++){
int link=fa[i];
while(link){
siz[link]-=siz[i];
link=fa[link];
}
int res1,res2;
pos=i;
Sum=inf;
get_ans(1,f[1]-f[i]-siz[i]*dep[i],siz[1]);
res1=Sum;
Sum=inf;
get_ans(i,f[i],siz[i]);
res2=Sum;
ans=min(ans,res1+res2);
link=fa[i];
while(link){
siz[link]+=siz[i];
link=fa[link];
}
}
write_endl(ans);
return 0;
}
[TJOI2011] 书架
普通 dp 式很容易想出来,令 \(f_i\) 表示到 \(i\) 的最小代价,\(s_i\) 表示 \(a_i\) 的前缀和。
可以发现的是随着 \(j\) 增大 \(\max\limits_{k=j+1}^ia_k\) 是不升的。于是我们可以比较自然地想到当 \(i\) 固定时,每个值作为 \(\max\) 的时间是一段区间。维护一个 \(mx\) 数组,每次在序列的最后新添一个值时,会修改一个区间的值。这个可以用线段树维护,同时可以用线段树维护 \(f_i+mx_i\) 的最小值。\(j\) 的最小值可以通过二分查找得到。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=1e5+10,inf=1e15;
int n,mx,s[N],a[N],f[N],b[N];
int stk[N],pre[N],top;
namespace Seg_Tree{
int ans[N<<2],f[N<<2],h[N<<2];
int ls(int p){
return p<<1;
}
int rs(int p){
return p<<1|1;
}
void push_up(int p){
ans[p]=min(ans[rs(p)],ans[ls(p)]);
f[p]=min(f[ls(p)],f[rs(p)]);
}
void push_down(int p){
if(h[p]){
ans[ls(p)]=f[ls(p)]+h[p];
ans[rs(p)]=f[rs(p)]+h[p];
h[ls(p)]=h[rs(p)]=h[p];
h[p]=0;
}
}
void build(int p,int l,int r){
ans[p]=inf;
if(l==r){
return;
}
int mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
}
void update_f(int p,int l,int r,int pos,int val){
if(l==r){
f[p]=val;
ans[p]=val+h[p];
return;
}
push_down(p);
int mid=(l+r)>>1;
if(pos<=mid){
update_f(ls(p),l,mid,pos,val);
}
else{
update_f(rs(p),mid+1,r,pos,val);
}
push_up(p);
}
void update_h(int p,int l,int r,int q_l,int q_r,int val){
if(q_l<=l&&r<=q_r){
ans[p]=f[p]+val;
h[p]=val;
return;
}
push_down(p);
int mid=(l+r)>>1;
if(q_l<=mid){
update_h(ls(p),l,mid,q_l,q_r,val);
}
if(q_r>mid){
update_h(rs(p),mid+1,r,q_l,q_r,val);
}
push_up(p);
}
int query(int p,int l,int r,int q_l,int q_r){
if(q_l<=l&&r<=q_r){
return ans[p];
}
push_down(p);
int mid=(l+r)>>1,res=inf;
if(q_l<=mid){
res=min(res,query(ls(p),l,mid,q_l,q_r));
}
if(q_r>mid){
res=min(res,query(rs(p),mid+1,r,q_l,q_r));
}
return res;
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
read(n),read(mx);
for(int i=1;i<=n;i++){
read(a[i]),read(b[i]);
s[i]=s[i-1]+b[i];
}
a[0]=inf;
for(int i=n;i>=0;i--){
while(top&&a[stk[top]]<a[i]){
pre[stk[top]]=i;
top--;
}
stk[++top]=i;
}
Seg_Tree::build(1,1,n);
for(int i=1;i<=n;i++){
Seg_Tree::update_f(1,1,n,i,f[i-1]);
Seg_Tree::update_h(1,1,n,pre[i]+1,i,a[i]);
int pos=lower_bound(s,s+n+1,s[i]-mx)-s;
f[i]=Seg_Tree::query(1,1,n,pos+1,i);
}
write_endl(f[n]);
return 0;
}
[THUPC2021]混乱邪恶
先观察一下六个方向的性质,发现朝三个角度相隔 \(\frac{2\pi}{3}\) 的方向各走一步可以回到原点,朝角度相隔 \(\pi\) 的两个方向各走一步也可以回到原点。一个等腰直角三角形也有这样的性质,只是距离不同。但因为最后要回到原点,我们对于距离这个东西没有特殊需求,我们需要的只是原坐标系的性质。于是我们放弃原来的六边形,将它改到直角坐标系上,将六个方向变为 \((0,1),(1,1),(1,0),(0,-1),(-1,-1),(-1,0)\)。接下来直接可行性 dp,令 \(f_{x,i,j,k,l}\) 表示走 \(x\) 步后,能否到达 \((i,j)\),且权值为 \((k,l)\)。为了去掉负坐标,原点为 \((100,100)\)初始 \(f_{0,100,100,0,0}\) 有值,最后判断 \(f_{0,100,100,L,G}\) 是否有值。因为只需要知道可行性,所以可以用 bitset 维护。
但复杂度过不去这一题。根据随机游走问题的结论,每次随机一个方向,走 \(n\) 步的期望距离不超过 \(\sqrt{n}\),本题的选择方案也可以看作是随机走一步,将 \(n\) 个 idea 随机一下,就可以将每个方向的移动范围开到 \(2\sqrt{n}\) 以内了。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=110;
int n,mod,d[20]={1,1,1,0,0,-1,-1,-1,-1,0,0,1};
struct node{
int b[20];
}a[N];
bitset<N>f[2][40][40][N];
void clear(int opt){
for(int i=0;i<=30;i++){
for(int j=0;j<=30;j++){
for(int k=0;k<mod;k++){
f[opt][i][j][k].reset();
}
}
}
}
void solve(int opt,int i,int j,int k,int di,int dj,int dk,int dl){
if(i+di<0||i+di>30||j+dj<0||j+dj>30){
return;
}
f[opt][i+di][j+dj][(k+dk)%mod]|=(f[opt^1][i][j][k]<<dl);
f[opt][i+di][j+dj][(k+dk)%mod]|=(f[opt^1][i][j][k]>>(mod-dl));
}
void work(int opt,int num){
for(int i=0;i<=30;i++){
for(int j=0;j<=30;j++){
for(int k=0;k<mod;k++){
if(f[opt^1][i][j][k].any()){
for(int l=0;l<12;l+=2){
solve(opt,i,j,k,d[l],d[l+1],a[num].b[l],a[num].b[l+1]);
}
}
}
}
}
}
mt19937 rnd(time(NULL));
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
read(n),read(mod);
for(int i=1;i<=n;i++){
for(int j=0;j<12;j++){
read(a[i].b[j]);
}
}
shuffle(a+1,a+n+1,rnd);
int L,G;
read(L),read(G);
int opt=0;
f[0][15][15][0][0]=1;
for(int i=1;i<=n;i++){
opt^=1;
clear(opt);
work(opt,i);
}
if(f[opt][15][15][L][G]){
puts("Chaotic Evil");
}
else{
puts("Not a true problem setter");
}
return 0;
}
[ABC180F] Unbranched
近期做过最简单的 dp 了。
根据题意,一个连通块只能是一个点数不超过 \(l\) 的链或环。先考虑一个选定 \(n\) 个点的链有多少种。显然直接填有 \(n!\) 种,又因为链是对称的,所以再除个 \(2\),即一个 \(n\) 个点的链有 \(\frac{n!}{2}\) 种。环是同理,又因为环可以旋转,每个位置都可以是起点,所以要再除以 \(n\),总方案数有 \(\frac{(n-1)!}{2}\) 种。为了固定连通块的出现顺序,防止重复,令当前没有选的第一个点为必选点,也就是只需要从未选择点中选择 \(|S|-1\) 个点即可。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pdi pair<double,int>
#define pb push_back
#define eps 1e-9
#define mp make_pair
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=310,mod=1e9+7,inv2=5e8+4;
int n,m,l,f[N][N],R[N],L[N],C[N][N];
int query(int l){
memset(f,0,sizeof(f));
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=1;k<=min(l,min(i,j+1));k++){
f[i][j]=(f[i][j]+1ll*C[n-i+k-1][k-1]*L[k]%mod*f[i-k][j-k+1]%mod)%mod;
}
for(int k=2;k<=min(l,min(i,j));k++){
f[i][j]=(f[i][j]+1ll*C[n-i+k-1][k-1]*R[k]%mod*f[i-k][j-k]%mod)%mod;
}
}
}
return f[n][m];
}
void solve(){
read(n),read(m),read(l);
C[0][0]=1;
R[2]=L[1]=inv2;
for(int i=1;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
}
for(int i=2;i<=n;i++){
L[i]=1ll*L[i-1]*i%mod;
}
for(int i=3;i<=n;i++){
R[i]=1ll*R[i-1]*(i-1)%mod;
}
R[2]=L[1]=1;
write_endl((query(l)-query(l-1)+mod)%mod);
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
int t=1;
while(t--){
solve();
}
return 0;
}
Anthem of Berland
令 \(f_i\) 表示区间 \([1,i]\) 内最多可以匹配多少次,考虑怎么转移。如果这个点不匹配,显然 \(f_i=f_{i-1}\),如果匹配上了也不代表 \(f_i=f_{i-m}+1\),因为是按照这里匹配的情况给 ?
赋字母,这样可能会带来更多的贡献。令 \(g_i\) 表示当前匹配时,\([1,i]\) 的匹配次数。使用 kmp
得到 \(t\) 的 \(nxt\) 数组,显然每次往前跳的位置都是有可能带来答案的,令当前跳到 \(t\) 串的 \(j\) 位置,\(g_i=\max (g_{i-m+j}+1,g_i)\),最后 \(f_i\) 取当前位是否形成一个匹配串中较大的即可。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=1e5+10;
char s[N],t[N];
int n,m,nxt[N],f[N],g[N];
void build(){
for(int i=2,j=0;i<=m;i++){
while(j&&t[i]!=t[j+1]){
j=nxt[j];
}
if(t[i]==t[j+1]){
j++;
}
nxt[i]=j;
}
}
bool chk(int pos){
if(pos<m){
return 0;
}
for(int i=1;i<=m;i++){
if(s[pos-i+1]=='?'){
continue;
}
if(s[pos-i+1]!=t[m-i+1]){
return 0;
}
}
return 1;
}
void solve(){
scanf("%s%s",s+1,t+1);
n=strlen(s+1),m=strlen(t+1);
build();
for(int i=1;i<=n;i++){
f[i]=f[i-1];
if(chk(i)){
g[i]=f[i-m]+1;
for(int j=nxt[m];j;j=nxt[j]){
g[i]=max(g[i-(m-j)]+1,g[i]);
}
f[i]=max(f[i],g[i]);
}
}
write_endl(f[n]);
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
int t=1;
while(t--){
solve();
}
return 0;
}
[AGC007D] Shik and Game
可以发现移动的策略是走到某只熊的位置开始掉头,拿完刚刚所有没拿的金币继续走。考虑证明这个策略的最优性,假设某个金币不拿走,那么后面某次再过来拿走时,必然会将这一次掉头的某一段路径再重复走一次,显然不优。
于是我们可以写出 \(O(n^2)\) 的 dp 式:
后面这个 \(\max\) 的意思为如果掉头到还没拿的第一个金币位置时,这个金币还未产出,要等到产出金币再继续往前走。
考虑优化这个式子,首先可以发现 \(x_i-x_j\) 可以提出来,这个部分的和就是 \(E\)。那么我们只需要分类讨论一下 \(\min\) 里面套的这个 \(\max\) 了。
当 \(2\times (x_i-x_{j+1})\le T\) 时,肯定是找到最小的满足这个条件的 \(j\),不然肯定是将等待时间浪费了。
当 \(2\times (x_i-x_{j+1})\ge T\) 时,拆开式子,求的是满足 \(f_j-x_{j+1}\times 2\) 最小的 \(j\),扫一遍维护即可。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pii pair<int,int>
#define pdi pair<double,int>
#define pb push_back
#define eps 1e-9
#define mp make_pair
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=1e5+10,inf=1e18;
int f[N],n,m,T,x[N];
void solve(){
read(n),read(m),read(T);
for(int i=1;i<=n;i++){
read(x[i]);
}
int l=0,mn=inf;
for(int i=1;i<=n;i++){
while((x[i]-x[l+1])*2>=T&&l<n){
mn=min(mn,f[l]-x[l+1]*2);
l++;
}
f[i]=mn+x[i]*2;
f[i]=min(f[i],f[l]+T);
}
write_endl(f[n]+m);
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
int t=1;
while(t--){
solve();
}
return 0;
}
Relay Race
容易发现,这是一道经典老题的升级版,但因为本人觉得这个优化还是有些学习的意义,所以写了下来。
先回顾一下,当 \(n\le 50\) 时是怎么做的。可以将问题看作两个人同时从 \((1,1)\) 走到 \((n,n)\),且每一步后 \(x\) 和 \(y\) 坐标均不能减小,于是写出 dp 方程式。令 \(f_{i,j,k,l}\) 表示第一个人走到 \((i,j)\),第二个人走到 \((k,l)\) 时,最大价值和。
可以发现dp方程式中每个时刻两个人的步数是相同的,而已知步数和横坐标可以得到两个点,所以令 \(f_{i,j,k}\) 表示走了 \(i\) 步,第一个人横坐标为 \(j\),第二个人的横坐标为 \(k\) 时,最大价值和。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
int n,f[610][310][310],a[310][310];
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
read(n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
read(a[i][j]);
}
}
memset(f,-0x3f,sizeof(f));
f[1][1][1]=a[1][1];
for(int i=2;i<n*2;i++){
for(int j=max(i-n+1,1);j<=min(i,n);j++){
for(int k=max(i-n+1,1);k<=min(i,n);k++){
int val;
if(j==k){
val=a[j][i-j+1];
}
else{
val=a[j][i-j+1]+a[k][i-k+1];
}
f[i][j][k]=max(max(f[i-1][j-1][k-1],f[i-1][j-1][k]),max(f[i-1][j][k-1],f[i-1][j][k]))+val;
}
}
}
write_endl(f[n*2-1][n][n]);
return 0;
}
[ARC083E] Bichrome Tree
挺有意思的一道题。
有一个容易得到的贪心是,当前点子树内,非当前点颜色的权值之和尽量小。考虑 dp,令 \(f_{u,0/1}\) 表示点 \(u\) 颜色为 \(0/1\) 时,另一种颜色的权值之和的最小值,\(g_{u,i,0/1}\) 表示 \(u\) 子树内 \(0/1\) 颜色的权值和为 \(i\) 时,另一种颜色的权值之和的最小值。
分类讨论,当 \(u\) 颜色为 \(0\) 时,\(v\) 颜色为 \(0\) 时,\(g_{u,j,0}=min(g_{u,j-x_v,0}+f_{v,0},g_{u,j,0})\),\(v\) 颜色为 \(1\) 时,\(g_{u,j,0}=min(g_{u,j,0},g_{u,j-f_{v,0},0})+x_v\)。\(u\) 颜色为 \(1\) 时同理。\(f_{u,i}=\min\limits_{j=0}^{x_u}g_{u,i,j}\)。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pdi pair<double,int>
#define pb push_back
#define eps 1e-9
#define mp make_pair
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=5010,inf=0x3f3f3f3f;
vector<int>e[N];
int n,x[N],f[2][N];
void dfs(int u){
if(!e[u].size()){
f[0][u]=f[1][u]=0;
return;
}
int g[2][N],h[2][N];
memset(h,0x3f,sizeof(h));
h[0][0]=h[1][0]=0;
for(auto v:e[u]){
dfs(v);
memset(g,0x3f,sizeof(g));
for(int i=x[u];i>=0;i--){
if(x[v]<=i){
g[0][i]=min(g[0][i],h[0][i-x[v]]+f[0][v]);
g[1][i]=min(g[1][i],h[1][i-x[v]]+f[1][v]);
}
if(f[1][v]<=i){
g[0][i]=min(g[0][i],h[0][i-f[1][v]]+x[v]);
}
if(f[0][v]<=i){
g[1][i]=min(g[1][i],h[1][i-f[0][v]]+x[v]);
}
}
swap(h,g);
}
for(int i=0;i<=x[u];i++){
f[0][u]=min(f[0][u],h[0][i]);
f[1][u]=min(f[1][u],h[1][i]);
}
}
void solve(){
read(n);
for(int i=2;i<=n;i++){
int fa;
read(fa);
e[fa].pb(i);
}
for(int i=1;i<=n;i++){
read(x[i]);
}
memset(f,0x3f,sizeof(f));
dfs(1);
if(f[0][1]==inf&&f[1][1]==inf){
puts("IMPOSSIBLE");
}
else{
puts("POSSIBLE");
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
int t=1;
while(t--){
solve();
}
return 0;
}
Vasya and Binary String
一道很酷的题。
先做一遍完全背包,使得对于每一个大的长度,直接删除的价值不劣于分为几个小长度再一起删除,方便后续的方程转移。
最容易想到的想法是区间 dp,令 \(f_{i,j,k,0/1}\) 表示区间 \([i,j]\) 的最后一次操作是删掉 \(k\) 个 \(0/1\) 的最大价值。转移就如同正常区间 dp 一样转移,时间复杂度 \(O(n^5)\)。
这个复杂度我们不能够接受,优化一下。可以发现区间 dp 的 \(O(n^3)\) 枚举区间转移肯定是变不了的,所以原 dp 的主要问题在于 \(O(n^2)\) 枚举两个区间最后一次删除操作是什么,再合并。
因为删去的都是同色的,所以考虑枚举有多少个和 \(i\) 同色的点,这次和 \(i\) 一起删去。得到一个新的 dp 状态,令 \(f_{i,j,k}\) 表示完全删去区间 \([i,j]\) 和并删除前面与 \(i\) 同色的 \(k\) 个点后的最大价值。有两种转移:
- 直接选择删除,相当于删除这一些与 \(i\) 同色的点并完全删除区间 \([i+1,r]\)。
- 选择在区间中找到另一个与 \(i\) 同色的点 \(p\),前面的 \(k+1\) 个点(包括 \(i\))选择和 \(p\) 一起删除。同时为了删除 \(p\) 和这 \(k+1\) 个点,我们需要先完全删掉区间 \([i+1,p-1]\)。
最后答案为 \(f_{1,n,0}\),总复杂度 \(O(n^4)\)。
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pii pair<int,int>
#define pdi pair<double,int>
#define pb push_back
#define eps 1e-9
#define mp make_pair
using namespace std;
namespace IO{
template<typename T>
inline void read(T &x){
x=0;
int f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+(ch-'0');
ch=getchar();
}
x=(f==1?x:-x);
}
template<typename T>
inline void write(T x){
if(x<0){
putchar('-');
x=-x;
}
if(x>=10){
write(x/10);
}
putchar(x%10+'0');
}
template<typename T>
inline void write_endl(T x){
write(x);
putchar('\n');
}
template<typename T>
inline void write_space(T x){
write(x);
putchar(' ');
}
}
using namespace IO;
const int N=110;
int f[N][N][N],n,a[N],val[N];
void solve(){
read(n);
for(int i=1;i<=n;i++){
char ch=getchar();
while(ch!='0'&&ch!='1'){
ch=getchar();
}
a[i]=ch-'0';
}
for(int i=1;i<=n;i++){
read(val[i]);
}
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
val[i]=max(val[i],val[i-j]+val[j]);
}
}
for(int len=1;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
for(int i=0;i<=n;i++){
f[l][r][i]=max(f[l][r][i],f[l+1][r][0]+val[i+1]);
for(int p=l+1;p<=r;p++){
if(a[l]==a[p]){
f[l][r][i]=max(f[l+1][p-1][0]+f[p][r][i+1],f[l][r][i]);
}
}
}
}
}
write_endl(f[1][n][0]);
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
int t=1;
while(t--){
solve();
}
return 0;
}
Brand New Problem
看到数据范围,一眼状压dp。令 \(f_{i,j}\) 表示前 \(j\) 个单词,包含的单词集合为 \(i\) 时逆序对的最小值,这个时间复杂度为 \(O(k2^n)\)。发现逆序对数量很少,想得到一个经典的套路,将值域改为定义域。
得到 \(f_{i,j}\) 表示集合为 \(i\) 且逆序对为 \(j\) 时,最后一个选的单词的位置的最小值。为了方便转移,令选择的顺序就是最后的序列顺序,这样我们就可以预处理得到在集合为 \(i\) 的情况下选择新增模板串的第 \(j\) 个单词时增加的逆序对个数。将匹配串的对应字符的位置存下来,用二分寻找转移前的位置之后的对应单词的位置的最小值,就可以实现 \(O(\log k)\) 转移。总复杂度 \(O(2^nn^2\log k)\)
点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pdi pair<double,int>
#define pb push_back
#define eps 1e-9
#define mp make_pair
using namespace std;
const int MX=(1<<15)+10,Sum=15*15/2,inf=1e9;
int f[MX][Sum],n,sum,num,add[MX][20];
string s;
map<string,int>id;
vector<int>pos[20];
void work(){
int cnt;
for(int i=1;i<=n;i++){
vector<int>().swap(pos[i]);
}
cin>>cnt;
for(int i=1;i<=cnt;i++){
cin>>s;
if(id.count(s)){
pos[id[s]].pb(i);
}
}
for(int i=0;i<num;i++){
for(int j=0;j<=n*(n-1)/2;j++){
f[i][j]=cnt+1;
}
}
f[0][0]=0;
for(int i=0;i<num;i++){
for(int j=0;j<=n*(n-1)/2;j++){
if(f[i][j]==cnt+1){
continue;
}
for(int k=1;k<=n;k++){
if(i>>(k-1)&1){
continue;
}
auto it=lower_bound(pos[k].begin(),pos[k].end(),f[i][j]);
if(it==pos[k].end()){
continue;
}
f[i|(1<<(k-1))][j+add[i][k]]=min(f[i|(1<<(k-1))][j+add[i][k]],*it);
}
}
}
for(int i=0;i<=n*(n-1)/2;i++){
if(f[num-1][i]!=cnt+1){
sum=i;
return;
}
}
sum=inf;
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>s;
id[s]=i;
}
num=(1<<n);
for(int i=0;i<num;i++){
for(int j=1;j<=n;j++){
if(i>>(j-1)&1){
continue;
}
for(int k=j+1;k<=n;k++){
if(i>>(k-1)&1){
add[i][j]++;
}
}
}
}
int m,mn=inf,p=0;
cin>>m;
for(int i=1;i<=m;i++){
sum=0;
work();
if(sum<mn){
mn=sum;
p=i;
}
}
if(!p){
cout<<"Brand new problem!\n";
}
else{
cout<<p<<'\n';
cout<<"[:";
mn=(n-1)*n/2-mn+1;
while(mn){
cout<<'|';
mn--;
}
cout<<":]\n";
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t=1;
while(t--){
solve();
}
return 0;
}