DP刷题笔记III

· · 个人记录

笔记I(1~50题)

笔记II(51~100题)

现在开始!

CI.[IOI2009]salesman

思想非常simple:因为一次从上游往下游的转移,可以被表示成

f_i+(pos_i-pos_j)\times U\rightarrow f_j\ |\ pos_i<pos_j\land tim_i<tim_j

拆开括号,即可得到两半互不相关的部分。然后直接使用线段树/树状数组进行转移即可。

从下游往上游的转移也可以类似地处理。

现在考虑tim中可能有相等的情形,并不能确定访问顺序。这个再使用一遍辅助DP过一遍就行了。有一个结论是当tim相等时,一次转移中一定不会走回头路——回头路的部分完全可以在上次转移和下次转移处就处理掉了。然后就直接DP过就行了。

3min就能想出的idea,我整整调了3d。主要因为一开始套了两重离散化,后来发现数据范围开的下便删去了离散化;一开始写的是线段树,后来发现线段树debug起来很麻烦,便换成了BIT;一开始也没有想到没有回头路的情形,辅助DP时写的极其憋屈(后来证明就是这个憋屈的DP中有一处UD写反了);同时中文题面翻译还翻译错了,这个“距离”是到上游的距离而非到下游的距离。于是种种因素叠加在一起,debug得精神崩溃。

代码:

#include<bits/stdc++.h>
using namespace std;
const int inf=0xc0c0c0c0;
const int N=1001000;
int n,U,D,S,tim[N],pos[N],bon[N],m=500100,ord[N],f[N],g[N],upper[N],lower[N];//upper:the maximal when go against the wave; lower:vice versa
void modify(int P,int val){
    for(int x=P;x;x-=x&-x)upper[x]=max(upper[x],val-P*U);
    for(int x=P;x<=m;x+=x&-x)lower[x]=max(lower[x],val+P*D);
}
int queryupper(int P){
    int ret=inf;
    for(int x=P;x<=m;x+=x&-x)ret=max(ret,upper[x]+P*U);
    return ret;
}
int querylower(int P){
    int ret=inf;
    for(int x=P;x;x-=x&-x)ret=max(ret,lower[x]-P*D);
    return ret;
}
#define I ord[i]
#define J ord[j]
#define K ord[k]
int main(){
    scanf("%d%d%d%d",&n,&U,&D,&S),memset(upper,0xc0,sizeof(upper)),memset(lower,0xc0,sizeof(lower));
    for(int i=1;i<=n;i++)scanf("%d%d%d",&tim[i],&pos[i],&bon[i]),ord[i]=i;
    sort(ord+1,ord+n+1,[](int x,int y){return tim[x]==tim[y]?pos[x]<pos[y]:tim[x]<tim[y];});
    modify(S,0);
    for(int i=1,j=1;j<=n;){
        while(tim[I]==tim[J])f[J]=g[J]=max(queryupper(pos[J]),querylower(pos[J]))+bon[J],j++;
        for(int k=i+1;k<j;k++)f[K]=max(f[K],f[ord[k-1]]-(pos[K]-pos[ord[k-1]])*D+bon[K]);
        for(int k=j-2;k>=i;k--)g[K]=max(g[K],g[ord[k+1]]-(pos[ord[k+1]]-pos[K])*U+bon[K]);
        while(i<j)modify(pos[I],max(f[I],g[I])),i++;
    }
    printf("%d\n",max(queryupper(S),querylower(S)));
    return 0;
}

CII.HDU6212 Zuma

一眼区间DP。

首先,我们将串压缩(即将相同颜色的相邻珠子合并)。记col_i为位置i的颜色,sz_i为位置i的珠子数。

我们设f[i,j]表示消去区间[i,j]中所有东西的最小步数。

则有:

f[i,j]=\min\begin{cases}3-sz_i&|i=j\\f[i,k]+f[k+1,j]&|i\leq k<j\\f[i+1,j-1]+\max(0,3-sz_i-sz_j)&|col_i=col_j\\f[i+1,k-1]+f[k+1,j-1]&|col_i=col_j=col_k,(sz_i\neq 2\lor sz_j\neq 2)\land sz_k=1\end{cases}

其中,第一条转移是直接补满3个球;第二条转移是找个地方切一刀;第三条转移是将ij最终合并在一起进行消除;第四条转移是将ij,以及区间中某一个k合并消除,但需要保证有一种消除顺序可以使得k可以先在不与某一边一起消掉的前提下消到那一边,然后再合并两边。

时间复杂度O(Tn^3),需要保证常数。

代码:

#include<bits/stdc++.h>
using namespace std;
int T,n,m,sz[210],f[210][210];
bool col[210];
char s[210];
int main(){
    scanf("%d",&T);
    for(int t=1;t<=T;t++){
        scanf("%s",s+1),m=strlen(s+1),n=0;
        col[1]=s[1]-'0',sz[1]=1,n++;
        for(int i=2;i<=m;i++){
            if(s[i]-'0'==col[n])sz[n]++;
            else n++,col[n]=s[i]-'0',sz[n]=1;
        }
        for(int i=1;i<=n;i++)f[i][i]=3-sz[i];
        for(int l=2;l<=n;l++)for(int i=1,j=i+l-1;j<=n;i++,j++){
            f[i][j]=0x3f3f3f3f;
            for(int k=i;k<j;k++)f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
            if(col[i]!=col[j])continue;
            f[i][j]=min(f[i][j],f[i+1][j-1]+max(0,3-sz[i]-sz[j]));
            if(sz[i]==2&&sz[j]==2)continue;
            for(int k=i+1;k<j;k++)if(col[k]==col[i]&&sz[k]==1)f[i][j]=min(f[i][j],f[i+1][k-1]+f[k+1][j-1]);
        }
        printf("Case #%d: %d\n",t,f[1][n]);
    }
    return 0;
} 

CIII.[APIO2014]连珠线

一般的换根DP题。

明显可以看出,最终的树一定可以通过指定一个根变成一棵有根树,所有的蓝边都可以被分成两两一组,其中每组中两条边深度递增。

于是我们可以设置DP状态。f_{x,0/1}表示节点x,它不是/是某对蓝边的中间节点时,子树中最大的蓝边权和。

简单使用multiset维护f_{x,1}从哪个儿子转移过来最优即可。

然后换个根即可。

代码:

#include<bits/stdc++.h>
using namespace std;
const int inf=0xc0c0c0c0;
int n,f[200100][2],head[200100],cnt,res;
struct node{
    int to,next,val;
}edge[400100];
void ae(int u,int v,int w){
    edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
multiset<int>s[200100];
void dfs1(int x,int fa){
    for(int i=head[x],y;i!=-1;i=edge[i].next){
        if((y=edge[i].to)==fa)continue;
        dfs1(y,x);
        int tmp=max(f[y][0],f[y][1]+edge[i].val);
        f[x][0]+=tmp;
        f[x][1]+=tmp,s[x].insert(f[y][0]+edge[i].val-tmp);
    }
    if(s[x].empty())f[x][1]=inf;
    else f[x][1]+=*s[x].rbegin();
//  printf("%d:%d %d\n",x,f[x][0],f[x][1]);
}
void dfs2(int x,int fa){
    for(int i=head[x],y;i!=-1;i=edge[i].next){
        if((y=edge[i].to)==fa)continue;
        int tmp=max(f[y][0],f[y][1]+edge[i].val);
        int fx0=f[x][0],fx1=f[x][1];
        fx0-=tmp;
        fx1-=tmp;
        fx1-=*s[x].rbegin();
        int pmt=f[y][0]+edge[i].val-tmp;
        s[x].erase(s[x].find(pmt));
        fx1=(s[x].empty()?inf:fx1+*s[x].rbegin());
        s[x].insert(pmt);

        int qwq=max(fx0,fx1+edge[i].val);
        f[y][0]+=qwq;
        f[y][1]=(s[y].empty()?0:f[y][1]-*s[y].rbegin());
        f[y][1]+=qwq;
        s[y].insert(fx0+edge[i].val-qwq);
        f[y][1]+=*s[y].rbegin();
        dfs2(y,x);
    }
}
int main(){
    scanf("%d",&n),memset(head,-1,sizeof(head));
    for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
    dfs1(1,0),dfs2(1,0);
    for(int i=1;i<=n;i++)res=max(res,f[i][0]);
    printf("%d\n",res);
    return 0;
}

CIV.[TopCoder 12519]ScotlandYard

我们考虑一个最原始的DP状态:f[\mathbb{S}]表示根据当前给出的信息,猜的人可以推测出当前藏的人一定在且仅在集合\mathbb{S}之中时,藏的人最多可以走多少步。

然后考虑枚举藏的人下一步给出走了什么颜色的边,然后取\mathbb{S}中所有点当前颜色的出边集合的并集\mathbb{T},则有f(\mathbb{S})\leftarrow f(\mathbb{T})+1。边界状态为 f(\varnothing)=0f(\mathbb{S})=1\text{ when }|\mathbb{S}|=1

观察可以发现,猜的人最终可以确定藏的人在哪里的前一刻,\mathbb{S}的大小:

  1. 如果\geq3,显然只判断其中两个亦可;

  2. 如果=2,显然可以根据此两个一路反推回去,即任意时刻|\mathbb{S}|都可以被缩减到2

  3. 如果\leq1,此状态显然不合法,不可能出现。

故我们发现任意时刻状态中仅需存储\mathbb{S}中两个数即可。这样状态数就缩小到O(n^2)级别了。则此时就可以直接按照上文所述DP了。采取记忆化搜索的方式DP,如果任意时刻发现搜索到成环了,则答案必为\infty

代码(TC的格式):

#include<bits/stdc++.h>
using namespace std;
class ScotlandYard{
private:
    const int inf=0x3f3f3f3f;
    int n,f[60][60];
    vector<int>g[60][3];
    bool in[60][60];
    int dfs(int x,int y){
        if(in[x][y])return inf;
        if(f[x][y]!=-1)return f[x][y];
        in[x][y]=true;
        f[x][y]=0;
        for(int i=0;i<3;i++){
            vector<int>v;
            for(auto j:g[x][i])v.push_back(j);
            for(auto j:g[y][i])v.push_back(j);
            sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
            if(v.size()==0){f[x][y]=max(f[x][y],0);continue;}
            if(v.size()==1){f[x][y]=max(f[x][y],1);continue;}
            for(int i=0;i<v.size();i++)for(int j=i+1;j<v.size();j++)f[x][y]=max(f[x][y],min(inf,dfs(v[i],v[j])+1));
        }
        in[x][y]=false;
        return f[x][y];
    }
public:
    int maxMoves(vector<string>taxi,vector<string>bus,vector<string>metro){
        n=taxi.size();
        for(int i=0;i<n;i++)for(int j=0;j<3;j++)g[i][j].clear();
        for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(taxi[i][j]=='Y')g[i][0].push_back(j);
        for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(bus[i][j]=='Y')g[i][1].push_back(j);
        for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(metro[i][j]=='Y')g[i][2].push_back(j);
//      for(int i=0;i<3;i++){for(int j=0;j<n;j++){for(auto k:g[j][i])printf("%d ",k);puts("");}puts("");}
        memset(f,-1,sizeof(f));
        int res=0;
        for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)res=max(res,dfs(i,j));
        return res>=inf?-1:res;
    }
}my;

CV.[ARC067D] Yakiniku Restaurants

明显在最优方案中,行走方式一定是从一条线段的一端走到另一端,不回头。

于是设 f[i,j] 表示从 i 走到 j 的最优代价。明显,该代价对于不同的券相互独立。故我们依次考虑每一张券。

我们发现,假设有一张位置 k 的券,则所有 k\in[l,r][l,r] 都是可以享受到它的。于是,我们建出笛卡尔树来,就可以把它用差分轻松解决了(假设笛卡尔树上有一个节点 x,它是区间 [l,r] 中的最大值,则所有区间 [l,r] 中穿过它的区间都会增加 a_x,但是它的两个子区间 [l,x-1][x+1,r] 却享受不到,故在该处再减少 a_x,即可实现差分地更新。

则时间复杂度 O(nm+n^2)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[210][5010],stk[5010],tp,lson[5010],rson[5010];
ll s[5010],f[5010][5010],res;
void solve(int id,int x,int l,int r,int las){
    f[l][r]+=a[id][x]-las;
    if(l==r)return;
    if(lson[x])solve(id,lson[x],l,x-1,a[id][x]);
    if(rson[x])solve(id,rson[x],x+1,r,a[id][x]);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=2;i<=n;i++)scanf("%lld",&s[i]),s[i]+=s[i-1];
    for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[j][i]);
    for(int i=1;i<=m;i++){
        tp=0;
        for(int j=1;j<=n;j++){
            lson[j]=rson[j]=0;
            while(tp&&a[i][stk[tp]]<=a[i][j])lson[j]=stk[tp--];
            if(stk[tp])rson[stk[tp]]=j;
            stk[++tp]=j;
        }
        solve(i,stk[1],1,n,0);
    }
    for(int i=1;i<=n;i++)for(int j=n;j>=i;j--)f[i][j]+=f[i-1][j]+f[i][j+1]-f[i-1][j+1];
    for(int i=1;i<=n;i++)for(int j=i;j<=n;j++)res=max(res,f[i][j]-(s[j]-s[i]));
    printf("%lld\n",res);
    return 0;
}

CVI.[CSACADEMY]Root Change

常规换根DP。设 f_i 表示 i 子树中以 i 为起点的最长路径长度,设 sz_i 表示 i 子树中的数量,再设 g_i 表示 i 子树的答案。

fsz 显然很好转移。考虑 g,则有

g_i=\begin{cases}sz_i&(\text{存在两个以上的儿子具有最长路径})\\sz_i+\Big(g_j-(sz_j+1)\Big)&(\text{具有最长路径的儿子}j\text{唯一})\end{cases}

于是直接上 multiset 暴力换根即可。时间复杂度 O(n\log n)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,f[100100],g[100100],sz[100100];
vector<int>v[100100];
multiset<pair<int,int> >s[100100];
multiset<int>t[100100];
void dfs1(int x,int fa){
    for(auto y:v[x])if(y!=fa)dfs1(y,x),f[x]=max(f[x],f[y]+1),sz[x]+=sz[y]+1,s[x].insert(make_pair(f[y]+1,g[y]-(sz[y]+1))),t[x].insert(f[y]+1);
    g[x]=sz[x];
    if(s[x].size()==1||s[x].size()>=2&&s[x].rbegin()->first!=(++s[x].rbegin())->first)g[x]+=s[x].rbegin()->second;
}
void dfs2(int x,int fa){
    for(auto y:v[x]){
        if(y==fa)continue;
        int fx=0,szx=sz[x]-sz[y]-1;
        t[x].erase(t[x].find(f[y]+1));
        if(!t[x].empty())fx=*t[x].rbegin();
        t[x].insert(f[y]+1);
        int gx=szx;
        s[x].erase(s[x].find(make_pair(f[y]+1,g[y]-(sz[y]+1))));
        if(s[x].size()==1||s[x].size()>=2&&s[x].rbegin()->first!=(++s[x].rbegin())->first)gx+=s[x].rbegin()->second;
        s[x].insert(make_pair(f[y]+1,g[y]-(sz[y]+1)));

        t[y].insert(fx+1);
        s[y].insert(make_pair(fx+1,gx-(szx+1)));
        sz[y]+=szx+1;
        f[y]=*t[y].rbegin();
        g[y]=sz[y];
        if(s[y].size()==1||s[y].size()>=2&&s[y].rbegin()->first!=(++s[y].rbegin())->first)g[y]+=s[y].rbegin()->second;
        dfs2(y,x);
    }
}
int main(){
    scanf("%d",&n);
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    dfs1(1,0),dfs2(1,0);
    for(int i=1;i<=n;i++)printf("%d\n",g[i]);
    return 0;
}

CVII.[NOI2009]二叉查找树

首先该树的中序遍历是唯一可以确定的(直接按照数据值排序即可)。

然后,因为权值可以被修改成一切实数,故我们完全可以把权值离散化掉。

于是我们现在可以设置一个DP状态f[l,r,lim]表示:

区间[l,r]中的所有东西构成了一棵子树,且树中最小权值不小于lim的最优方案。

然后就枚举根转移即可。转移的时候就可以看作是子树内所有东西被整体提高了一层,所以直接增加sum[l,r](意为区间[l,r]中的所有数据值之和)即可。同时,如果有当前枚举的根的权值不小于lim,显然就可以不修改,但是两边儿子的权值就必须比它大;否则则必须修改,两边儿子的权值下限还是lim(因为根的权值可以被修改成一个略大于lim的实数)。

则时间复杂度O(n^4)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,sum[110],f[110][110][110];
struct dat{
    int val,key,lam;
}a[100];
int dfs(int l,int r,int lim){
    if(l>r)return 0;
    if(f[l][r][lim]!=-1)return f[l][r][lim];
    int &now=f[l][r][lim];now=0x3f3f3f3f;
    for(int i=l;i<=r;i++){//assume that i is the root in the section [l,r].
        if(a[i].key>=lim)now=min(now,dfs(l,i-1,a[i].key)+dfs(i+1,r,a[i].key)+sum[r]-sum[l-1]);//do not modify, the height simply increased by one.
        now=min(now,dfs(l,i-1,lim)+dfs(i+1,r,lim)+m+sum[r]-sum[l-1]);//modify i to any real number a little greater than lim.
    }
    return now;
}
int main(){
    scanf("%d%d",&n,&m),memset(f,-1,sizeof(f));
    for(int i=1;i<=n;i++)scanf("%d",&a[i].val);
    for(int i=1;i<=n;i++)scanf("%d",&a[i].key);
    for(int i=1;i<=n;i++)scanf("%d",&a[i].lam);
    sort(a+1,a+n+1,[](dat u,dat v){return u.key<v.key;});
    for(int i=1;i<=n;i++)a[i].key=i;
    sort(a+1,a+n+1,[](dat u,dat v){return u.val<v.val;});
    for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i].lam;
    printf("%d\n",dfs(1,n,1));
    return 0;
}

CVIII.[POI2014]MRO-Ant colony

根据下取整除法的性质(\left\lfloor\dfrac{\left\lfloor\dfrac{x}{y}\right\rfloor}{z}\right\rfloor=\left\lfloor\dfrac{x}{yz}\right\rfloor),我们可以反向考虑,即从特殊边开始,计算出从每个叶子到特殊边的路径上,要除以的那个分母是什么。

这个可以直接一遍dfs就出来了(可以把它当成DP)。注意,当一段路径的分母已经爆10^9时就可以直接退出了,因为这样子不会有蚂蚁到得了特殊边。

然后,对于一个分母d,所有\in\Big[dk,d(k+1)\Big)的蚁群数量都是合法的;故我们直接对蚁群数量排序然后二分再差分即可。

时间复杂度O(n\log n)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int LIM=1e9;
int n,m,k,sp[1001000],U,V;
ll dif[1001000],res;
vector<int>v[1001000],u;
void dfs(int x,int fa,int lam){
    if(v[x].size()==1){u.push_back(lam);return;}
    if(1ll*lam*(v[x].size()-1)>LIM)return;
    lam*=(v[x].size()-1);
    for(auto y:v[x])if(y!=fa)dfs(y,x,lam);
}
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=m;i++)scanf("%d",&sp[i]);
    scanf("%d%d",&U,&V),v[U].push_back(V),v[V].push_back(U);
    for(int i=1,x,y;i+1<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    dfs(U,V,1),dfs(V,U,1);
    sort(sp+1,sp+m+1);
    for(auto i:u){
        ll l=1ll*k*i,r=1ll*(k+1)*i;
        if(l>LIM)continue;
        dif[lower_bound(sp+1,sp+m+1,l)-sp]++;
        dif[lower_bound(sp+1,sp+m+1,r)-sp]--;
    }
    for(int i=1;i<=m;i++)dif[i]+=dif[i-1],res+=dif[i]; 
    printf("%lld\n",res*k);
    return 0;
}

CIX.[NOI Online #1 入门组]魔法

我们可以构造出原图的转移矩阵 A,表示只走原图的边的代价。这个直接暴力上Floyd即可。

我们还可以构造出魔法的转移矩阵B

则,可以想到,答案一定是

ABABABABAB\dots ABA

这种样子。

故我们用B左乘A得到C=(BA)。则计算AC^k,即为答案。

时间复杂度O(n^3\log k)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,p;
struct Matrix{
    ll a[110][110];
    void init(){memset(a,0x3f,sizeof(a));for(int i=1;i<=n;i++)a[i][i]=0;}
    ll* operator[](int x){return a[x];}
    friend Matrix operator *(Matrix &u,Matrix &v){
        Matrix w;w.init();
        for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=1;k<=n;k++)w[i][j]=min(w[i][j],u[i][k]+v[k][j]);
        return w;
    }
}a,b;
int main(){
    scanf("%d%d%d",&n,&m,&p);
    a.init(),b.init();
    for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),a[x][y]=min(a[x][y],1ll*z),b[x][y]=min(b[x][y],-1ll*z);
    for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)a[i][j]=min(a[i][j],a[i][k]+a[k][j]);
//  for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",a[i][j]);puts("");}
    b=b*a;
//  for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",b[i][j]);puts("");}
    for(;p;p>>=1,b=b*b)if(p&1)a=a*b;
    printf("%lld\n",a[1][n]);
    return 0;
}

CX.[POI2015]MOD

比较恶心的题目。

首先,有一个结论,即如果把两棵树通过某种方式连接起来,新树的直径的端点一定来自于原本两棵树的直径端点集合。

则考虑新树的最大直径,明显就是把两棵树的直径直接连一块,就是两棵树的直径之和再加一。

考虑新树的最小直径,则应该选择两树直径的中点(如果直径长度为奇数则随便选一个)连一块,这样新树的直径就是 \left\lceil\dfrac{\text{直径一}}{2}\right\rceil+\left\lceil\dfrac{\text{直径二}}{2}\right\rceil+1。当然,还得与两条直径本身取一个\max

于是我们就用换根DP求出g_x表示x子树内的直径,h_x求出其子树外的直径(这里换根DP我一开始用的是multiset维护,但是会莫名其妙MLE。故最后不得不全面换成vector才不出问题)。然后两个拼一块就能找出所有新树的直径的最大值/最小值。

代码(非常丑陋):

#include<bits/stdc++.h>
using namespace std;
int n,f[500100],g[500100],h[500100],FA[500100],mx,mn=0x3f3f3f3f;
//f[i]:the maximal length starting from i; g[i]: the maximal length in i's subtree; h[i]:the maximal length outside that.
vector<int>v[500100],s[500100],t[500100];
void dfs1(int x,int fa){
    for(auto y:v[x]){
        if(y==fa)continue;
        FA[y]=x;
        dfs1(y,x);
        f[x]=max(f[x],f[y]+1);
        s[x].push_back(f[y]+1);
        t[x].push_back(g[y]);
        g[x]=max(g[x],g[y]);
    }
    sort(s[x].rbegin(),s[x].rend());while(s[x].size()>3)s[x].pop_back();
    sort(t[x].rbegin(),t[x].rend());while(t[x].size()>2)t[x].pop_back();
    if(s[x].size()>=2)g[x]=max(g[x],s[x][0]+s[x][1]);
    else if(s[x].size()>=1)g[x]=max(g[x],s[x][0]);
}
void dfs2(int x,int fa){
    int alls=0;
    for(auto i:s[x])alls+=i;
//  printf("%dS",x);for(auto i:s[x])printf("%d ",i);puts("");
//  printf("%dT",x);for(auto i:t[x])printf("%d ",i);puts("");
    for(auto y:v[x]){
        if(y==fa)continue;
        if(f[y]+1>=s[x].back())h[y]=alls-(f[y]+1);
        else if(s[x].size()<=2)h[y]=alls;
        else h[y]=s[x][0]+s[x][1];

        if(t[x][0]!=g[y])h[y]=max(h[y],t[x][0]);
        else if(t[x].size()>=2)h[y]=max(h[y],t[x][1]);

        t[y].push_back(h[y]);
        sort(t[y].rbegin(),t[y].rend());while(t[y].size()>2)t[y].pop_back();

        if(s[x][0]!=f[y]+1)s[y].push_back(s[x][0]+1);
        else if(s[x].size()>=2)s[y].push_back(s[x][1]+1);
        else s[y].push_back(1);
        sort(s[y].rbegin(),s[y].rend());while(s[y].size()>3)s[y].pop_back();

        dfs2(y,x);
    }
}
int S,dp,inva,nd,rt;
void dfs3(int x,int fa,int dep){
    if(dep>dp)S=x,dp=dep;
    for(auto y:v[x])if(y!=fa&&y!=inva)dfs3(y,x,dep+1);
}
bool dfs4(int x,int fa){
    if(x==S){
        nd--;
        if(nd==0)rt=x;
        return true;    
    }
    for(auto y:v[x]){
        if(y==fa)continue;
        if(!dfs4(y,x))continue;
        nd--;
        if(nd==0)rt=x;
        return true;
    }
    return false;
}
int main(){
    scanf("%d",&n);
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    dfs1(1,0),dfs2(1,0);
//  for(int i=1;i<=n;i++)printf("%d:%d %d %d\n",i,f[i],g[i],h[i]);
    for(int i=2;i<=n;i++)mx=max(mx,g[i]+h[i]+1),mn=min(mn,max({(g[i]+1)/2+(h[i]+1)/2+1,g[i],h[i]}));
    for(int i=2;i<=n;i++){
        if(mn!=max({(g[i]+1)/2+(h[i]+1)/2+1,g[i],h[i]}))continue;
        printf("%d %d %d ",mn,i,FA[i]);
        inva=FA[i];
        S=0,dp=-1;
        dfs3(i,FA[i],0);
        int T=S;
        S=0,dp=-1;
        dfs3(T,0,0);
        nd=(g[i]+2)/2;
        dfs4(T,0);
        printf("%d ",rt);

        inva=i;
        S=0,dp=-1;
        dfs3(FA[i],i,0);
        T=S;
        S=0,dp=-1;
        dfs3(T,0,0);
        nd=(h[i]+2)/2,dfs4(T,0);
        printf("%d\n",rt);
        break;
    }
    for(int i=2;i<=n;i++){
        if(mx!=g[i]+h[i]+1)continue;
        inva=0;
        printf("%d %d %d ",mx,i,FA[i]);
        S=0,dp=-1;
        dfs3(i,FA[i],0);
        printf("%d ",S);
        S=0,dp=-1;
        dfs3(FA[i],i,0);
        printf("%d\n",S);
        break;
    }
    return 0;
}

CXI.[九省联考2018]一双木棋chess

一下子就想到了LXX.[USACO5.5]贰五语言Two Five(可见刷题笔记II),因为同是阶梯型的图样。然后稍微想一想就发现总方案数可以用隔板法证得是\dbinom{n+m}{m}的,代入一看发现才2\times10^5都不到。于是就果断DP了。

首先先用爆搜搜出所有图案的分布(实现从编号到图案的映射),然后再预处理一个辅助的DP来实现从图案到编号的映射。然后就直接分当前是谁操作进行不同的转移即可。

时间复杂度,如上所述,是\dbinom{n+m}{m}\times\text{转移复杂度}的。我采取的转移是O(n^2)的,还可以被优化为最终O(n)转移,但是没有必要。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,cnt,f[20][20],a[20][20],b[20][20],g[200000];
vector<int>v[200100];
vector<int>u;
void dfs(int pos,int lim){
    if(pos==n){v[++cnt]=u;return;}
    for(int i=0;i<=lim;i++){
        u.push_back(i);
        dfs(pos+1,i);
        u.pop_back();
    }
}
int deco(vector<int>&ip){
    int ret=1;
    for(int i=0;i<n;i++)if(ip[i])ret+=f[i][ip[i]-1];
    return ret;
}
int dp(int ip,bool sd){
    if(ip==cnt)return 0;
    if(g[ip]!=-1)return g[ip];
    if(sd==0){//first player
        g[ip]=0xc0c0c0c0;
        vector<int>tmp=v[ip];
        for(int i=0;i<n;i++){
            if(i==0&&tmp[i]==m||i>0&&tmp[i]==tmp[i-1])continue;
            tmp[i]++;
            g[ip]=max(g[ip],dp(deco(tmp),sd^1)+a[i][tmp[i]]);
            tmp[i]--;
        }
        return g[ip];
    }else{
        g[ip]=0x3f3f3f3f;
        vector<int>tmp=v[ip];
        for(int i=0;i<n;i++){
            if(i==0&&tmp[i]==m||i>0&&tmp[i]==tmp[i-1])continue;
            tmp[i]++;
            g[ip]=min(g[ip],dp(deco(tmp),sd^1)-b[i][tmp[i]]);
            tmp[i]--;
        }
        return g[ip];       
    }
}
int main(){
    scanf("%d%d",&n,&m),memset(g,-1,sizeof(g));
    dfs(0,m);
    for(int i=0;i<=m;i++)f[n-1][i]=i+1;
    for(int i=n-2;i>=0;i--)for(int j=0;j<=m;j++)for(int k=0;k<=j;k++)f[i][j]+=f[i+1][k];
    for(int i=0;i<n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);
    for(int i=0;i<n;i++)for(int j=1;j<=m;j++)scanf("%d",&b[i][j]);
    printf("%d\n",dp(1,0));
    return 0;
} 

CXII.[CEOI2007]树的匹配Treasury

题解

CXIII.[JLOI2016/SHOI2016]侦察守卫

神题。

见代码即可。

#include<bits/stdc++.h>
using namespace std;
int n,m,p,a[500100],f[500100][25],g[500100][25],res=0x3f3f3f3f;
//f[i,j]:minimum cost when there're at most j layers left uncovered
//g[i,j]:minimum cost when there're at least j layers outside the subtree covered
bool sp[500100];
vector<int>v[500100];
void dfs(int x,int fa){
    if(sp[x])f[x][0]=g[x][0]=a[x];//at special points, empty state still need a guard; at normal points, empty state doesn't need a guard.
    for(int i=1;i<=m;i++)g[x][i]=a[x];//at whatever points, a state which can spread outside the subtree need a guard.
    g[x][m+1]=0x3f3f3f3f;//avoiding transferring outside bounds
    for(auto y:v[x]){
        if(y==fa)continue;
        dfs(y,x);
        for(int i=m;i>=0;i--)g[x][i]=min(g[x][i]+f[y][i],f[x][i+1]+g[y][i+1]);
        //in this case transferring order doesn't matter.
        //but g's transferring must take place before f since it needs f in transferring.
        //case 1:guards in x spread into y, where there are i layers downwards y is covered from x.
        //case 2:guards in y spread into x, where there are i+1 layers downward x is covered from y, and y can also cover upper levels.
        for(int i=m;i>=0;i--)g[x][i]=min(g[x][i],g[x][i+1]);
        //in this case it is getting a suffix minimum, where transferring order matters.
        f[x][0]=g[x][0];//in fact f[x][0] and g[x][0] have the same meaning(which is a full subtree covered and nothing more)
        for(int i=1;i<=m;i++)f[x][i]+=f[y][i-1];
        //if there are at most i layers downwards, there should be at most i-1 layers downwards.
        for(int i=1;i<=m;i++)f[x][i]=min(f[x][i],f[x][i-1]);
        //getting a prefix minimum.
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    scanf("%d",&p);
    for(int i=1,x;i<=p;i++)scanf("%d",&x),sp[x]=true;
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    dfs(1,0);
    printf("%d\n",f[1][0]);
    return 0;
}

CXIV.[POI2014]ZAL-Freight

题解

CXV.[COCI2019]Mobitel

如果正着来DP的话,状态是 O(rsn) 的,不可能通过。

这时,我们就要应用一些数论知识了:

\prod a_i<n

\left\lfloor\dfrac{n-1}{\prod a_i}\right\rfloor\geq 1

然后,整除又有如下性质:

\left\lfloor\dfrac{n-1}{\prod\limits_{i=1}^ka_i}\right\rfloor=\left\lfloor\dfrac{\left\lfloor\dfrac{n-1}{\prod\limits_{i=1}^{k-1}a_i}\right\rfloor}{a_k}\right\rfloor

同时,依据整除分块的性质,\left\lfloor\dfrac{n}{i}\right\rfloor的值域大小是 O(\sqrt{n})

因此我们可以保存上述下取整的结果作为DP状态。此时复杂度就来到了 O(rs\sqrt{n})

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,lim,p,a[310][310],f[2][310][2010],deco[2010],code[1001000];
int main(){
    scanf("%d%d%d",&n,&m,&lim),lim--;
    deco[1]=lim,code[lim]=1,p=1;
    for(int i=2;i<=lim;i++)if(lim/i!=deco[p])p++,deco[p]=lim/i,code[lim/i]=p;
    for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);
//  for(int i=1;i<=p;i++)printf("%d ",deco[i]);puts("");
    f[n&1][m][code[lim/a[n][m]]]=1;
    for(int i=n;i;i--){
        memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
        for(int j=m;j;j--)for(int k=0;k<=p;k++){
            if(i-1)(f[!(i&1)][j][code[deco[k]/a[i-1][j]]]+=f[i&1][j][k])%=mod;
            if(j-1)(f[i&1][j-1][code[deco[k]/a[i][j-1]]]+=f[i&1][j][k])%=mod;
        }   
    }
    printf("%d\n",f[1][1][0]);
    return 0;
}

CXVI.[COCI2014-2015#1] Kamp

一看题面,突然感觉很弱智,不就是求出以每个点为根到其它所有特殊点的距离之和吗?这不是随随便便换个根就完事了吗?

然后兴冲冲敲出来,一测样例全挂。

后来发现并不是这样的,因为车上可以同时搭载多人,且车最后可以就停在某个地方不回去了。

稍微想想可以发现,最终停着的位置,一定是离起点最远的特殊点;故我们直接使用 multiset 维护一下就可以换根了。求出每个节点离其最远的特殊点的距离后,以它为根的答案就是(\text{从它出发到达所有点再回到它的最短距离})-(\text{上述DP值})

然后,因为车上可以搭载多人,所以实际上上述最短距离就是二倍以当前点和所有特殊点构成的虚树大小。这个可以直接通过求虚树大小的做法(按照dfs序排序再求出两两相邻点间距离)或者干脆直接再来一发换根解决。

时间复杂度O(n\log n)

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,head[500100],cnt,g[500100];
ll f[500100],h[500100];
bool sp[500100];
struct node{
    int to,next,val;
}edge[1001000];
void ae(int u,int v,int w){
    edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
multiset<ll>s[500100];
void dfs1(int x,int fa){
    if(sp[x])g[x]++,h[x]=0,s[x].insert(0);
    for(int i=head[x],y;i!=-1;i=edge[i].next){
        if((y=edge[i].to)==fa)continue;
        dfs1(y,x),f[x]+=f[y]+1ll*!!g[y]*edge[i].val,g[x]+=g[y];
        h[x]=max(h[x],h[y]+edge[i].val),s[x].insert(h[y]+edge[i].val);
    }
}
void dfs2(int x,int fa){
    for(int i=head[x],y;i!=-1;i=edge[i].next){
        if((y=edge[i].to)==fa)continue;
        ll fx=f[x]-f[y]-1ll*!!g[y]*edge[i].val;
        int gx=g[x]-g[y];
        f[y]+=fx+1ll*!!gx*edge[i].val;
        g[y]+=gx;

        s[x].erase(s[x].find(h[y]+edge[i].val));
        if(!s[x].empty())s[y].insert(*s[x].rbegin()+edge[i].val);
        s[x].insert(h[y]+edge[i].val);
        h[y]=*s[y].rbegin();

        dfs2(y,x);
    }
}
int main(){
    scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),memset(h,0xc0,sizeof(h));
    for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
    for(int i=1,x;i<=m;i++)scanf("%d",&x),sp[x]=true;
    dfs1(1,0),dfs2(1,0);
//  for(int i=1;i<=n;i++)printf("%lld %d %lld\n",f[i],g[i],h[i]);
    for(int i=1;i<=n;i++)printf("%lld\n",2*f[i]-h[i]); 
    return 0;
}

CXVII.[清华集训2012]串珠子

如果直接暴力上状压进行计数是会重复计算的;那么怎样不重不漏地计数呢?

我们发现,要求出连通图的数量是比较难的;但是要求出非联通图的数量是比较简单的,因为我们可以祭出套路。

我们设 f_i 表示 i 集合中所有图的数量(不管联通与否)。再设 g_i 表示非联通图的数量,h_i 表示连通图的数量。

则显然,必有 h_i=f_i-g_i

首先,f_i显然是很好求出的;在g_i中,我们可以考虑枚举ilowbit所在的连通块,然后就把它拆成一个 h_j\times f_{i\setminus j}了。

则时间复杂度O(3^n)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,a[20][20],f[1<<20],g[1<<20],h[1<<20];
//f:all possible situations
//g:all invalid situations
//h:all valid situations
int main(){
    scanf("%d",&n),m=1<<n;
    for(int i=0;i<n;i++)for(int j=0;j<n;j++)scanf("%d",&a[i][j]);
    f[0]=1;
    for(int i=1;i<m;i++){
        f[i]=f[i^(i&-i)];
        for(int j=__builtin_ctz(i)+1;j<n;j++)if(i&(1<<j))f[i]=1ll*f[i]*(a[j][__builtin_ctz(i)]+1)%mod;
    }
    for(int i=1;i<m;i++){
        for(int U=i^(i&-i),V=U;V;V=(V-1)&U)(g[i]+=1ll*h[i^V]*f[V]%mod)%=mod;
        h[i]=(f[i]-g[i]+mod)%mod;
    }
    printf("%d\n",h[m-1]);
    return 0;
}

CXVIII.[BJOI2017]机动训练

这题的瓶颈,在于把 a_i^2 看作 \sum\limits_{i=1}^{a_i}\sum\limits_{j=1}^{a_i}1,然后我们就可以看成“两两相同的机动路径都能贡献1”。于是我们设f_{x1,y1,x2,y2}表示两条起点为(x1,y1)(x2,y2)的相同路径的数量,然后分别枚举两条路径的方向(左上/左下/右下/右上)即可DP。

但需要注意的是,平行于坐标轴的方向会被重复计算,所以我们应该容斥一下。

代码:

#include<bits/stdc++.h>
using namespace std;
#define y1 _19260817
#define y2 _17680321
const int mod=1e9+9;
int n,m,f[31][31][31][31],dx[8]={1,1,0,-1,-1,-1,0,1},dy[8]={0,1,1,1,0,-1,-1,-1},res,d1,d2,lim;
int dx1[31],dy1[31],dx2[31],dy2[31];
char s[32][32];
int dfs(int x1,int y1,int x2,int y2){
    int &now=f[x1][y1][x2][y2];
    if(now!=-1)return now;
    if(s[x1][y1]!=s[x2][y2])return now=0;
    now=1;
    for(int i=1;i<=lim;i++){
        int X1=x1+dx1[i],Y1=y1+dy1[i],X2=x2+dx2[i],Y2=y2+dy2[i];
        if(X1>=1&&X1<=n&&Y1>=1&&Y1<=m&&X2>=1&&X2<=n&&Y2>=1&&Y2<=m)(now+=dfs(X1,Y1,X2,Y2))%=mod;
    }
    return now;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%s",s[i]+1);
    for(int d1=0;d1<8;d1++)for(int d2=0;d2<8;d2++){
        memset(f,-1,sizeof(f)),lim=0;
        for(int i=(d1&1?(d1+7)%8:d1);(i+7)%8!=(d1&1?(d1+1)%8:d1);i++,i%=8)for(int j=(d2&1?(d2+7)%8:d2);(j+7)%8!=(d2&1?(d2+1)%8:d2);j++,j%=8){
            lim++;
            dx1[lim]=dx[i],dy1[lim]=dy[i];
            dx2[lim]=dx[j],dy2[lim]=dy[j];
        }
        int lam=((d1&1)^(d2&1))?-1:1;
        for(int x1=1;x1<=n;x1++)for(int y1=1;y1<=m;y1++)for(int x2=1;x2<=n;x2++)for(int y2=1;y2<=m;y2++)(res+=(mod+lam*dfs(x1,y1,x2,y2))%mod)%=mod;
    }
    printf("%d\n",res);
    return 0;
}

CXIX.[SHOI2009]舞会

之前一直在往二项式反演去想,没想到最后居然成了……

我们考虑将男生和女生全部按照高度递减排序,则对于第i个男生,能与他构成特殊对的女生必定是一个前缀,设前缀长度为num_i。显然,num_i是单调不降的。

然后,我们考虑设f_i表示钦定i对匹配,其余随意的方案数。则f_i作一个二项式反演即可得到恰好匹配i对的方案数,再做一个前缀和就是匹配至多i对的方案数。

我们考虑DP。设f_{i,j}表示钦定前i个男生匹配j对特殊对的方案数,则我们要求的就是f_n的数组。

考虑第i人。因为他要么匹配一对特殊对,共有 num_i-j 种方案(前面的人匹配的目标当前的人也一定能匹配,所以就只剩num_i-j对了),要么干脆就不是钦定的对。

于是就有f_{i-1,j}\times(num_i-j)\rightarrow f_{i,j+1}f_{i-1,j}\rightarrow f_{i,j}

然后,最终得到f_{n,i}数组,再乘上一个(n-i)!就是我们一开始提出的f数组,因为剩下的位置没有被钦定,可以随便排。

套上高精度,时间复杂度O(n^2\times\text{高精度复杂度})

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,a[310],b[310],num[310];
struct Wint:vector<int>{
    Wint(){clear();}
    Wint(int x){clear();while(x)push_back(x%10),x/=10;}
    void operator ~(){
        for(int i=0;i+1<size();i++){
            (*this)[i+1]+=(*this)[i]/10,(*this)[i]%=10;
            while((*this)[i]<0)(*this)[i]+=10,(*this)[i+1]--;
        }
        while(!empty()&&back()>9){
            int tmp=back()/10;
            back()%=10;
            push_back(tmp);
        }
        while(!empty()&&!back())pop_back();
    }
    void operator +=(const Wint &x){
        if(size()<x.size())resize(x.size());
        for(int i=0;i<x.size();i++)(*this)[i]+=x[i];
        ~*this;
    }    
    void operator -=(const Wint &x){
        for(int i=0;i<x.size();i++)(*this)[i]-=x[i];
        ~*this;
    }
    friend Wint operator +(Wint x,const Wint &y){
        x+=y;
        return x;
    }
    friend Wint operator *(const Wint &x,const Wint &y){
        Wint z;
        if(!x.size()||!y.size())return z;
        z.resize(x.size()+y.size()-1);
        for(int i=0;i<x.size();i++)for(int j=0;j<y.size();j++)z[i+j]+=x[i]*y[j];
        ~z;
        return z;
    }
    void print()const{
        if(empty()){putchar('0');return;}
        for(int i=size()-1;i>=0;i--)putchar('0'+(*this)[i]);
    }
}C[310][310],f[310][310],fac[310];
signed main(){
    scanf("%d%d",&n,&m);
    fac[0]=1;
    for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i;
    for(int i=0;i<=n;i++)C[i][0]=1;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)C[i][j]=C[i-1][j-1]+C[i-1][j];
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    sort(a+1,a+n+1),reverse(a+1,a+n+1);
    for(int i=1;i<=n;i++)scanf("%d",&b[i]);
    sort(b+1,b+n+1),reverse(b+1,b+n+1);
    for(int i=1,j=1;i<=n;i++){
        while(j<=n&&b[j]>a[i])j++;
        num[i]=j-1;
    }
    f[0][0]=1;
    for(int i=0;i<n;i++)for(int j=0;j<=i;j++){
        if(j<=num[i+1])f[i+1][j+1]+=f[i][j]*(num[i+1]-j);
        f[i+1][j]+=f[i][j];
    }
    for(int i=0;i<=n;i++)f[n][i]=f[n][i]*fac[n-i];
    for(int i=n;i>=0;i--)for(int j=i+1;j<=n;j++)f[n][i]-=C[j][i]*f[n][j];
//  for(int i=0;i<=n;i++)printf("%d ",f[n][i]);puts("");
    for(int i=1;i<=n;i++)f[n][i]+=f[n][i-1];
    f[n][m].print();
    return 0;
}

CXX.CF917D Stranger Trees

这里是本题的DP解法。矩阵树定理解法详见矩阵树定理学习笔记中重题III.TopCoder13369-TreeDistance。

首先,一个基础结论是,如果一张 n 个点的图,被连成一棵森林,则继续加边连成一棵树的方案数是 n^{k-2}\prod\limits_{i=1}^kc_i,其中森林中共有 k 棵树,第 i 棵树的大小是 c_i

然后,考虑二项式反演。设 g_i 是至多有 i 条边在原树上的方案数。二项式反演一下即可得到恰好有 i 条边在原树上的方案数。则问题被转化为求出 g 数组。g 数组中 n^{k-2} 的部分是可以提出来统一计算的。考虑使用DP来求出 g 的另一部分。设 f_{i,j,k} 表示 i 的子树中被截成 j 棵树,且 i 节点自身所在树大小为 k 的权值(不包括 i 自身的树)和。合并节点 i 和儿子 i' 时,只需对二者处于同一连通块或是不同连通块进行树上背包分开转移即可。

时间复杂度 O(n^4),是树上背包复杂度。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,f[110][110][110],g[110],sz[110],h[110][110],fac[110],inv[110];
vector<int>v[110];
int ksm(int x,int y=mod-2){
    int z=1;
    for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;
    return z;
}
void dfs(int x,int fa){
    sz[x]=1,f[x][1][1]=1;
    for(auto y:v[x]){
        if(y==fa)continue;
        dfs(y,x);
        for(int i=1;i<=sz[x];i++)for(int I=1;I<=sz[y];I++)for(int j=1;j<=sz[x];j++)for(int J=1;J<=sz[y];J++){
            (h[i+I][j]+=1ll*f[x][i][j]*f[y][I][J]%mod*J%mod)%=mod;
            (h[i+I-1][j+J]+=1ll*f[x][i][j]*f[y][I][J]%mod)%=mod;
        }
        for(int i=1;i<=sz[x]+sz[y];i++)for(int j=1;j<=sz[x]+sz[y];j++)f[x][i][j]=h[i][j],h[i][j]=0;
        sz[x]+=sz[y];
    }
}
int C(int x,int y){return 1ll*fac[x]*inv[y]%mod*inv[x-y]%mod;}
int main(){
    scanf("%d",&n);
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
    inv[n]=ksm(fac[n]);for(int i=n-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
    dfs(1,0);
    for(int i=0,k=ksm(n);i<n;i++,k=1ll*k*n%mod){
        for(int j=1;j<=n;j++)(g[i]+=1ll*f[1][i+1][j]*j%mod)%=mod;
        g[i]=1ll*g[i]*k%mod;
    }
    for(int i=0;i<n;i++)for(int j=0;j<i;j++)(g[i]+=mod-1ll*g[j]*C(n-j-1,i-j)%mod)%=mod;
    for(int i=0;i<n;i++)printf("%d ",g[n-i-1]);
    return 0;
}

CXXI.[GYM100134I][NEERC2012]Identification of Protein

debug5h,精神崩溃。

首先,很容易想到把所有东西都乘上 10^5 变成整数。然后,因为 \gcd(9705276,12805858)=2,所以在字符串长度 \leq400 时,每个值被表示成二者倍数之和的方式是唯一的。于是我们可以把所有合法的值唯一转换成“有 p_iPq_iQ”的表达形式。因此,整条字符串中所具有的 P 数和 Q 数也就确定了。分别设其为 nm

然后,一条前缀就唯一对应了一条后缀,反之亦然。于是我们便可以建出一张矩阵,a_{i,j} 表示有多少个串中有 iPjQ(当然,作为前缀和后缀)。此时,一条字符串就对应了一条 (0,0)\rightarrow(n,m) 的路径,而所有可以被表示成其前缀或后缀的值的数量就是路径经过所有位置的权值和。

但是,一个值如果同时作为前缀和后缀出现,是不能被计算两遍的。所以我们设计DP状态时,要同时记录下正着的和反着的位置,以在上述情况时及时判断出来。我们设 f_{i,j,k} 表示当前填到前后缀各第 i 个字符,且前缀里已经填了 jP,后缀里已经填了 kQ 的最优收益。采取记忆化搜索进行DP,复杂度 O(n^3)

要注意一堆细节,例如实际上当总串长度为奇或偶时,前后缀相遇处时的处理是不一样的。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int P=9705276;
const int Q=12805858;
const int inf=-0x3f3f3f3f;
int q,n,m,s,g[410][410],f[210][210][210],opt[210][210][210];
ll a[100100],mx;
double db;
pair<int,int>pr;
pair<int,int>pt(ll ip){
    for(int i=0;ip>=0;ip-=P,i++)if(!(ip%Q))return make_pair(i,ip/Q);
    return make_pair(-1,-1);
}
int dfs(int stp,int x1,int x2){
    int &now=f[stp][x1][x2];
    if(now!=-1)return now;
    if((x1+x2-(s&1)>n||stp-x1+stp-x2-(s&1)>m)||!(x1<=n&&x2<=n&&stp-x1<=m&&stp-x2<=m))return now=inf;
    now=g[x1][stp-x1];
    if(x1!=x2&&(x1!=n-x2||stp-x1!=m-(stp-x2)))now+=g[x2][stp-x2];
    if(stp==((s+1)>>1))return now;
    if(x1+x2>n||stp-x1+stp-x2>m)return now=inf;
    int A=dfs(stp+1,x1,x2);
    int B=((s&1)&&(stp==(s>>1)))?inf:dfs(stp+1,x1+1,x2);
    int C=((s&1)&&(stp==(s>>1)))?inf:dfs(stp+1,x1,x2+1);
    int D=dfs(stp+1,x1+1,x2+1);
    int M=max({A,B,C,D});
    now+=M;
    if(A==M)opt[stp][x1][x2]=1;
    else if(B==M)opt[stp][x1][x2]=2;
    else if(C==M)opt[stp][x1][x2]=3;
    else if(D==M)opt[stp][x1][x2]=4;
    return now;
}
char res[410];
int main(){
    freopen("identification.in","r",stdin);
    freopen("identification.out","w",stdout);
    scanf("%d",&q),memset(f,-1,sizeof(f));
    for(int i=1;i<=q;i++){
        scanf("%lf",&db);
        a[i]=db*100000+0.1;
        mx=max(mx,a[i]);
    }
    pr=pt(mx),n=pr.first,m=pr.second,s=n+m;
    for(int i=1;i<=q;i++){
        pr=pt(a[i]);
        if(pr==make_pair(-1,-1))continue;
        if(pr.first>n||pr.second>m)continue;
        g[pr.first][pr.second]++;
        if((pr.first<<1)!=n||(pr.second<<1)!=m)g[n-pr.first][m-pr.second]++;
    }
    dfs(0,0,0);
    for(int stp=0,x1=0,x2=0;stp+1<=s-stp;stp++){
        if(opt[stp][x1][x2]==1)res[stp+1]='Q',res[s-stp]='Q';
        else if(opt[stp][x1][x2]==2)res[stp+1]='P',res[s-stp]='Q',x1++;
        else if(opt[stp][x1][x2]==3)res[stp+1]='Q',res[s-stp]='P',x2++;
        else if(opt[stp][x1][x2]==4)res[stp+1]='P',res[s-stp]='P',x1++,x2++;
    }
    printf("%s\n",res+1);
    return 0;
}

CXXII.CF913E Logical Expression

题解

CXXIII.CF612F Simba on the Circle

题解

CXXIV.[GYM102155J]stairways

首先,考虑暴力 n^3 DP——设 f_{i,j,k} 表示当前DP到第 i 个人,且第一条楼梯上到的最晚的人在时刻 j 到达,第二条楼梯在时刻 k

然后,观察到 j,k 中至少有一个值为前缀 \max 的时刻值。故状态可以被压缩到二维——f_{i,j} 表示第一条楼梯上的人是前缀 \max,第二条楼梯在时刻 k

设前缀 maxs。然后有

f_{i,j}=\begin{cases}f_{i-1,j}+s-a_i&(j<a_i)\\\min\limits_{j\leq a_i}f_{i-1,j}&(j=a_i)\\f_{i-1,j}+j-a_i&(j>a_i)\end{cases}

考虑 f_{i,j},其中肯定应该是 j 越小,值越大,即 f_{i,j} 应是单调递减的。假如在某个地方单调性被破坏,那肯定是在后方的东西更劣,可以直接舍去。

于是我们考虑全程只使用一个 f 数组,随着 i 增加改变里面的东西。设 f' 表示老的 f 数组,然后,假设我们已经保证了 f 的单调性,则转移式可以直接变为:

f_{j}=\begin{cases}f_{j}'+s-a_i&(j<a_i)\\f_{k}'&(j=a_i,\text{k是小于等于j的最大元素})\\f_{j}'+j-a_i&(j>a_i)\end{cases}

然后,a_i 可以在最后统一减去,优化得

f_{j}=\begin{cases}f_{j}'+s&(j<a_i)\\f_{k}'+a_i&(j=a_i,\text{k是小于等于j的最大元素})\\f_{j}'+j&(j>a_i)\end{cases}

于是我们现在要解决两个问题,一是区间加定值 s,这个简单用个什么线段树平衡树之类轻松解决;关键是第二个问题,后缀全部增加 j,同时还要维护单调性。

我们对于每个位置 j,设小于它的最大元素是 k,维护一个值为 \left\lfloor\dfrac{f_k-f_j}{j-k}\right\rfloor。当后缀带权加时,明显这个值会减少 1。而当这个值最终减少到 0 时,就说明单调性被破坏,可以删掉该元素了。

在前缀加 s 时,上述值并没有被改变;在修改 f_{a_i} 的值时,只有 a_i 附近的东西被改变了,暴力修改即可;在后缀带权加时,除了起始的地方,其他地方的值是全部减一的,于是仍然暴力修改起始处的值即可。

可以使用线段树,但这样就没法很方便地维护单调性;无论是分块还是平衡树维护都可以起到简单维护单调性的功效,直接上即可。

时间复杂度 O(n\sqrt{n})n\log n,取决于使用哪种数据结构。这里采取平衡树。

代码:

/*
f[i]=f[j] (j is the maximal element smaller than i
f[j]=f[j]+s-i (j<i)
f[j]=f[j]+j-i (j>i)

tms=upper[(f[k]-f[j])/(j-k)] where k<j.
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,a[100100],rt,cnt;
#define lson t[x].ch[0]
#define rson t[x].ch[1]
struct Treap{
    int key,rd,ch[2],sz;
    ll f,tagf,tagt,tms,mn;
}t[100100];
void pushup(int x){
    t[x].mn=t[x].tms,t[x].sz=1;
    if(lson)t[x].mn=min(t[x].mn,t[lson].mn),t[x].sz+=t[lson].sz;
    if(rson)t[x].mn=min(t[x].mn,t[rson].mn),t[x].sz+=t[rson].sz;
}
void ADDF(int x,ll y){if(x)t[x].tagf+=y,t[x].f+=y;}
void ADDT(int x,ll y=1){if(x)t[x].tagt+=y,t[x].tms-=y,t[x].mn-=y,t[x].f+=1ll*y*t[x].key;}
void pushdown(int x){
    ADDF(lson,t[x].tagf),ADDT(lson,t[x].tagt);
    ADDF(rson,t[x].tagf),ADDT(rson,t[x].tagt);
    t[x].tagf=t[x].tagt=0;
}
int merge(int x,int y){
    if(!x||!y)return x+y;
    if(t[x].rd>t[y].rd){pushdown(x),t[x].ch[1]=merge(t[x].ch[1],y),pushup(x);return x;}
    else{pushdown(y),t[y].ch[0]=merge(x,t[y].ch[0]),pushup(y);return y;}
}
void splitbykey(int x,int k,int &u,int &v){//u:<k.
    if(!x){u=v=0;return;}
    pushdown(x);
    if(t[x].key<k)u=x,splitbykey(rson,k,rson,v);
    else v=x,splitbykey(lson,k,u,lson);
    pushup(x);
}
void splitbysize(int x,int k,int &u,int &v){
    if(!x){u=v=0;return;}
    pushdown(x);
    if(t[lson].sz>=k)v=x,splitbysize(lson,k,u,lson);
    else u=x,splitbysize(rson,k-t[lson].sz-1,rson,v);
    pushup(x);
}
int Newnode(int key,ll f){
    int x=++cnt;
    t[x].f=f,t[x].key=key,t[x].rd=rand()*rand();
    pushup(x);
    return x;
}
ll flor(ll x,ll y){//the floor-rounded of x/y.
    if(x<=0)return 0;
    return (x-1)/y+1;
}
void func(int k,int pre){
    int a,b,c,d,e;
    splitbykey(rt,k,b,c);
    ADDF(b,pre);
    splitbysize(b,t[b].sz-1,a,b);
    splitbykey(c,k+1,c,d);
    if(!c)c=Newnode(k,0),t[c].f=t[b].f+k-pre;//because f[b] has already been added by pre.
    else t[c].f+=k;
    t[c].tms=flor(t[b].f-t[c].f,t[c].key-t[b].key);
    pushup(c);
    ADDT(d);
    splitbysize(d,1,d,e);
    if(d)t[d].tms=flor(t[c].f-t[d].f,t[d].key-t[c].key),pushup(d);
    rt=merge(merge(merge(merge(a,b),c),d),e);
}
int getmn(int x){
    pushdown(x);
    if(lson&&t[lson].mn<=0)return getmn(lson);
    if(t[x].tms<=0)return t[x].key;
    return getmn(rson);
}
void reset(){
    while(rt&&t[rt].mn<=0){
        int k=getmn(rt);
        int a,b,c,d,e;
        splitbykey(rt,k,b,c);
        splitbysize(b,t[b].sz-1,a,b);
        splitbykey(c,k+1,c,d);
        splitbysize(d,1,d,e);
        if(d)t[d].tms=flor(t[b].f-t[d].f,t[d].key-t[b].key),pushup(d);
        rt=merge(merge(a,b),merge(d,e));
    }
}
ll getres(){
    int a,b;
    splitbysize(rt,t[rt].sz-1,a,b);
    ll tmp=t[b].f;
    rt=merge(a,b);
    return tmp;
}
void iterate(int x){
    if(!x)return;
    pushdown(x);
    iterate(lson);
    printf("%d:(F:%d TMS:%d MN:%d)\n",t[x].key,t[x].f,t[x].tms,t[x].mn);
    iterate(rson);
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    rt=Newnode(0,a[1]),t[rt].tms=0x3f3f3f3f;
    for(int i=2,j=a[1];i<=n;i++){
        j=max(j,a[i]);
        func(a[i],j);
//      puts("BEF:");iterate(rt);
        reset();
//      puts("AFT:");iterate(rt);puts("");
    }
    ll res=getres();
    for(int i=1;i<=n;i++)res-=a[i];
    printf("%lld\n",res);
    return 0;
}
/*
9
44 43 42 41 33 66 32 11 5
*/

CXXV.[Topcoder16346]TwoPerLine

跟一年半以前就刷过的经典老题[AHOI2009]中国象棋完全一致,道理非常simple,设 f_{i,j,k} 表示DP到第 i 列,其中有 j 行内恰有 2 枚棋,k 行里恰有 1 枚棋,然后就依据第 i 列里填的东西填到哪进行转移即可。复杂度 O(n^3)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
class TwoPerLine{
private:
    int f[210][210][210];
    void add(int &x,int y){x+=y;if(x>=mod)x-=mod;}
    int sqr(int x){return (1ll*x*(x-1)/2)%mod;}
public:
    int count(int n,int m){
        f[0][0][0]=1;
        for(int i=0;i<n;i++)for(int j=0;j<=n;j++)for(int k=0;j+k<=n;k++)if(j*2+k<=m){
            add(f[i+1][j][k],f[i][j][k]);//put nothing on this colomn
            if(k)add(f[i+1][j+1][k-1],1ll*f[i][j][k]*k%mod);//put one chess on an 1-row
            if(k>=2)add(f[i+1][j+2][k-2],1ll*f[i][j][k]*sqr(k)%mod);//put both chesses on 1-rows
            if(j+k+1<=n)add(f[i+1][j][k+1],1ll*f[i][j][k]*(n-j-k)%mod);//put one chess on a 0-row
            if(j+k+2<=n)add(f[i+1][j][k+2],1ll*f[i][j][k]*sqr(n-j-k)%mod);//put both chesses on 0-rows
            if(k&&j+k+1<=n)add(f[i+1][j+1][k],1ll*f[i][j][k]*k%mod*(n-j-k)%mod);//put one chess on an 1-row, the other on a 0-row
        }
        int res=0;
        for(int j=0;j<=n;j++)for(int k=0;j+k<=n;k++)if(j*2+k==m)add(res,f[n][j][k]);
        return res;
    }
}my;
/*
int main(){
//  printf("%d\n",my.count(3,6));
//  printf("%d\n",my.count(7,0));
//  printf("%d\n",my.count(100,3));
//  printf("%d\n",my.count(6,4));
    return 0;
}
*/

CXXVI.[GYM102832J]Abstract Painting

考虑将一个圆心为 (x,0),半径为 R 的圆,转换为 x 轴上线段 [x-R,x+R],问题转换为求无交的线段覆盖方案数。

因为所有的圆半径很小(5),所以我们考虑状压位置 i 前面 10 位的信息(在实际应用中会发现只要状压 9 位即可,因为第 i-1 位必然为 0),表示第 i 位能否作为一个圆的左端点。当我们在位置 i 加入一个圆 [l,i] 时,需要保证当前状压的状态中第 l 位为 0l\geq0i-l\leq10 的偶数。在加入这么一个圆后,第 l+1\sim i-1 位都不能作为圆的左端点,更新状态即可。同时,因为一个位置可以作为不止一个圆的右端点,所以还得枚举作为哪些圆的右端点。因为所有可能的直径只有 2,4,6,8,10,所以直接 2^5 枚举即可。记得判断加入的圆的集合是否包含了必须加入的圆的集合!

总复杂度 n\times2^9\times2^5,可以通过。

(附,通过子集枚举等trick可以将复杂度优化到 3^5\times 2^5,但二者实际效率只不过差了个大约 2 的常数,而且不加也能过,就不加了)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,mus[1010],f[1010][1<<9],all=(1<<0)|(1<<2)|(1<<4)|(1<<6)|(1<<8),res;
int fob(int ip){
    int lim=1;
    while(lim<=ip)lim<<=1;
    return lim-1;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),mus[x+y]|=(1<<((y-1)<<1));
    f[0][0]=1;
    for(int i=1;i<=n;i++)for(int j=all;;j=(j-1)&all){
        if((j&mus[i])==mus[i]&&j<(1<<min(i-1,9))){
            int J=fob(j);
//          printf("%d:%d:%d\n",i,j,J);
            for(int k=0;k<(1<<min(i-1,9));k++){
                if(k&j)continue;
                (f[i][((k<<1)&((1<<9)-1))|J]+=f[i-1][k])%=mod;
            }           
        }
        if(!j)break;
    }
    for(int i=0;i<(1<<9);i++)(res+=f[n][i])%=mod;
    printf("%d\n",res);
    return 0;
} 

CXXVII.[GYM102822I]Invaluable Assets

引理1.最优解法下我们会尽量选取效果为 \sqrt{c} 的肥料。

考虑每袋肥料单位效果所需费用——此为 \dfrac{x^2+c}{x}。将分数拆开并套上均值,得到最大值在 \sqrt{c} 处取到。但因为肥料只能选取整数值,所以我们找到其上取整和下取整中更优的一个作为最值,设其为 p。则,从 1\sim p-1,单位费用不增;从 p+1\sim\infty,单位费用不降。(考虑对勾函数的图像即可)

引理2.最优解法中不会使用效果大于等于 2p 的肥料。

显然,对于一个效果大于等于 2p 的肥料,从中拆出一包肥料单独以 p 的效果使用,单位费用减少了;因此最多只会使用效果小于 2p 的肥料。

引理3.至多使用两种不同肥料。

考虑假设我们有两包肥料 i,j 在最优方案中被使用,则一定可以通过把它们各自向中间移动一位使得单位费用减少(因为对勾函数是凹函数)。不断移动,最终会得到结论——我们最多只会使用两种不同肥料,且其效果相差 1

引理4.不存在若干个和为 p 倍数的肥料。

这很显然,因为你明显可以用很多包 p 肥料来代替。

引理5.选择的非 p 肥料的费用不大于 3c

结合引理2、3、4,我们会发现,我们最多选择 p-1 个小于 2p 的不同非 p 肥料(不然必存在和为 p 倍数的肥料组)。(p-1)(2p-1)\leq3c 是一定成立的。

则我们考虑暴力背包求出需要肥料总量不大于 3c 的最优费用。依照上述结论,共有 2p-1 种不同物品,此部分复杂度 O(pc)=O(c\sqrt{c})

现在,考虑回答询问。假设对于一次询问,有一棵树需要生长 k 单位,则若 k\leq3c,显然我们可以直接通过DP数组回答;否则,考虑找出与 k 关于 p 同余且不大于 3c 的最大数,则答案即为该数的DP值+剩余部分全部用 p 填充的费用。

考虑将所有询问按照模 p 的余数分到 p 个同余类里,并在每个同余类中通过扫描线维护答案。因为数据随机,所以此部分也可以通过。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T,n,m,q,p,f[30100],h[100100],lim[100100],equ[110],cost;
ll res[100100];
vector<int>v[10100];
ll query(int now,int las){
    ll ret=0;
    int l1=lower_bound(h+1,h+n+1,now)-h;
    int l2=lower_bound(h+1,h+n+1,las-2*m)-h;
    for(int i=l2;i<l1;i++){
        int tmp=now-h[i];
        if(tmp<=3*m)ret+=f[tmp];
        else ret+=f[equ[tmp%p]]+1ll*(tmp-equ[tmp%p])/p*cost;
        if(h[i]<=las)ret-=f[las-h[i]];
    }
    ret+=1ll*((now-las)/p)*cost*(l2-1);
    return ret;
}
int main(){
    scanf("%d",&T);
    for(int tt=1;tt<=T;tt++){
        scanf("%d%d%d",&n,&m,&q),p=sqrt(m);
        if((p*p+m)*(p+1)>((p+1)*(p+1)+m)*p)p++;
//      printf("%d\n",p);
        cost=p*p+m;
        memset(f,0x3f,sizeof(f)),f[0]=0;
        for(int i=1;i<=2*p;i++)for(int j=i;j<=3*m;j++)f[j]=min(f[j],f[j-i]+i*i+m);
//      for(int i=0;i<=3*m;i++)printf("%d ",f[i]);puts("");
        for(int i=1;i<=n;i++)scanf("%d",&h[i]);sort(h+1,h+n+1);
//      for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
        for(int i=1;i<=q;i++)scanf("%d",&lim[i]),v[lim[i]%p].push_back(i);
        for(int i=m-p+1;i<=m;i++)equ[i%p]=i;
        for(int i=0;i<p;i++){
            if(v[i].empty())continue;
            sort(v[i].begin(),v[i].end(),[](int x,int y){return lim[x]<lim[y];});
            res[v[i][0]]=query(lim[v[i][0]],0);
            for(int j=1;j<v[i].size();j++)res[v[i][j]]=res[v[i][j-1]]+query(lim[v[i][j]],lim[v[i][j-1]]);
            v[i].clear();
        }
        printf("Case #%d:\n",tt);
        for(int i=1;i<=q;i++)printf("%lld\n",res[i]);
    }
    return 0;
}

CXXVIII.[AGC020E] Encoding Subsets

这种“压缩”题可以考虑区间DP。但是若考虑标准的区间的话它“子集”等定义又不好处理。

于是我们考虑对字符串作DP。设 f(S) 表示一个串 S 及其所有子集的压缩方案数。

显然,其有两种转移方式:一种是 S_0 不压缩,直接从 f(S_{1\sim|S|-1}) 转移过来;一种则是 S_0 与后面一些东西压缩。

于是我们枚举循环节长度以及循环次数,找到这所有循环串的交集(仍是一个串),然后转移即可。

使用 map 维护DP状态,复杂度玄学,但能过。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
string s;
void merge(string &s,string t){//merge string t into s
    for(int i=0;i<s.size();i++)if(t[i]=='0')s[i]='0';
}
map<string,int>mp;
int dfs(string s){
    if(s.empty())return 1;
    if(s.size()==1)return s[0]=='0'?1:2;
    if(mp.find(s)!=mp.end())return mp[s];
    int ret=1ll*dfs(s.substr(0,1))*dfs(s.substr(1))%mod;//encoding the leading character separately 
    for(int i=1;(i<<1)<=s.size();i++){//encoding i charactets together as a permutation chapter 
        string t=s.substr(0,i);
        for(int j=i;j+i<=s.size();j+=i)merge(t,s.substr(j,i)),(ret+=1ll*dfs(t)*dfs(s.substr(j+i))%mod)%=mod;
    }
    return mp[s]=ret;
}
int main(){
    cin>>s;
    printf("%d\n",dfs(s));
    return 0;
}

CXXIX.CF559E Gerald and Path

考虑将所有线段按照固定的那一端从小往大排序,并且对线段的端点离散化。

这之后,设 f_{i,j} 表示当前处理到线段 i,且所有线段中最右的那根的右端点不右于位置 j(即可以在 j 左面或与 j 重合)时的最优答案。

我们考虑,假设我们放了一根线段 [l,r]。因为不知道将来会放什么东西把它盖掉一部分,所以我们干脆在放线段 [l,r] 时,同时也放下线段 [l,l],[l,l+1],[l,l+2],\dots,[l,r-1],这样就不用担心被盖掉等讨论了。

于是我们现在考虑处理第 i 根线段。设其向左是 [l,p],向右是 [p,r]

首先,其有可能在接下来(或者在 i 之前就已经)被完全覆盖掉。于是一开始就有 f_{i-1,j}\rightarrow f_{i,j}

其次,考虑其向右摆。向右摆,就意味着最右位置一定是 r——若最右位不是 r,则一定在 i 之前还有一条向右摆的线段 [p',r'] 满足 r'\geq r。但是因为我们已经按照 p 递增排序了,故必有 [p,r]\subset[p',r'],即 [p,r] 被完全覆盖,我们已经在上面说过了。

则有 f_{i,r}\leftarrow f_{i-1,p}+[p,r]——因为我们已经令 f_{i,j} 表示“不右于”的最优值,所以就不用费尽心思枚举 i-1 时的最右位置了。同时,也不用担心重叠问题,因为按照我们上述讨论,为了避免重叠,我们直接将线段 [l,r] 看作了所有 [l,l\sim r]

但是这也意味着,我们的线段 [p,r] 也要被看作是所有的 [p,p\sim r]。于是我们枚举 j\in[p,r],则有 f_{i,j}\leftarrow f_{i-1,p}+[p,j]

然后就考虑其向左摆了。向左摆,就意味着最右位置不一定是 p——因为完全可以存在一条 [p',r'],满足 r'>p\operatorname{and}p'>l,即两者无交。

首先,最右位是 p 的就可以跟之前一样类似地转移,不在话下。

然后,对于最右位不是 p 的,我们枚举一个 j<i,表示最右位是 j 对应的 [p',r'](明显这里上述 r'>p\operatorname{and}p'>l 应被满足)。则所有 k\in(j,i] 都应该向左摆,因为若其向右摆,要么右端点在 r' 左边或与 r' 重合,被完全包含;要么右端点在 r' 右边,则 r' 就不是最右位了。

设所有 k\in(j,i] 的东西中,最左的那个是 [l'',p''],则整个 [j,i] 中所有线段,加一块可以变成一个 [l'',r'] 的大线段,且该线段是最右的。于是此处就可以跟前面一样,枚举一个 k\in[l'',r'] 进行转移了。

时间复杂度 O(n^3),因为枚举了 i,j,k 三个元素。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[110][310];
pair<int,int>p[110];
vector<int>v;
int L[110],P[110],R[110],res;
#define bs(x) lower_bound(v.begin(),v.end(),x)-v.begin()+1
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d%d",&p[i].first,&p[i].second),v.push_back(p[i].first),v.push_back(p[i].first-p[i].second),v.push_back(p[i].first+p[i].second);
    sort(p+1,p+n+1),sort(v.begin(),v.end()),v.resize(m=unique(v.begin(),v.end())-v.begin());
    for(int i=1;i<=n;i++)L[i]=bs(p[i].first-p[i].second),P[i]=bs(p[i].first),R[i]=bs(p[i].first+p[i].second);
    for(int i=1;i<=n;i++){
        memcpy(f[i],f[i-1],sizeof(f[i]));
        for(int j=P[i];j<=R[i];j++)f[i][j]=max(f[i][j],f[i-1][P[i]]+v[j-1]-v[P[i]-1]);
        for(int j=i;j;j--){
            int Rmax=(j==i?P[j]:R[j]);
            if(Rmax<P[i])continue;
            int Lmin=(j==i?L[j]:P[j]);
            for(int k=j+1;k<=i;k++)Lmin=min(Lmin,L[k]);
            for(int k=Lmin;k<=Rmax;k++)f[i][k]=max(f[i][k],f[j-1][Lmin]+v[k-1]-v[Lmin-1]);
        }
        for(int j=1;j<=m;j++)f[i][j]=max(f[i][j],f[i][j-1]);
    }
    for(int i=1;i<=m;i++)res=max(res,f[n][i]);
    printf("%d\n",res);
    return 0;
}

CXXX.[GYM102904B]Dispatch Money

考虑设 f_i 表示长度为 i 的前缀的最优划分。则我们发现,有 f_j+\operatorname{inversion}(j+1,i)\rightarrow f_i,其中 \text{inversion} 是区间逆序对数。

区间逆序对数不好处理,但是我们考虑像莫队一样处理,则其可以轻松地使用树状数组,从任何一个 \operatorname{inversion}(l,r) 求出 \operatorname{inversion}(l',r')

类莫队转移是分治优化DP的配套。但是分治优化DP要求转移具有单调性,此处明显 \text{inversion} 函数满足四边形不等式,可以使用分治优化。

分治优化还得有一个前提,即转移分层,形式化来说就是转移形如 g_j+\operatorname{inversion}(j+1,i)\rightarrow f_i,与本题结构不同。

但是,在分治的外面再套一个CDQ分治,即可使得转移分层——总是左子区间的 f 转移给右子区间的 f

于是复杂度 O(n\log^3n),其中的三个 \log 分别来自于CDQ、分治优化、树状数组。明显三个玩意常数都贼小,于是可以卡过。

但我们还不满足。现在考虑优化。

明显优化只能从树状数组下手。于是问题转换为能否 O(1) 地进行莫队的区间端点移动。考虑区间端点移动时,我们查询了区间中大于/小于某个数的元素个数,其可以被差分转换为前缀大于/小于查询。

考虑分块预处理。我们在值域上每 \sqrt n 分一个块,维护块内元素个数。与此同时,我们再在块间以及各个块内部作前缀和。这样,在往前缀内加入新元素时,就只需 O(\sqrt n) 地重构块内前缀和,再 O(\sqrt{n}) 地重构块间前缀和,这样就能 O(1) 地回答询问(散块内部前缀和、整块前缀和都可以 O(1) 得到)。

但是这样的查询是离线的。只需要对其可持久化即可做到在线询问。因为每次加入一个数,只会变动 O(\sqrt n) 级别的元素,所以时空复杂度都是 O(n\sqrt{n})。这样就可以 O(1) 进行端点移动,总复杂度为 O(n\sqrt n+n\log^2n)

这里因为笔者不会 O(1) 对数组可持久化,因此直接使用 O(n\log^3n) 暴力水过去了。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[300100],t[300100];
void ADD(int x,int tp){while(x)t[x]+=tp,x-=x&-x;}
int SUM(int x){int ret=0;while(x<=n)ret+=t[x],x+=x&-x;return ret;}
int LL=1,RR;
ll sum,f[300100];
void PushL(int x){
    sum+=SUM(1)-SUM(a[x]);
    ADD(a[x],1);
}
void PopL(int x){
    ADD(a[x],-1);
    sum-=SUM(1)-SUM(a[x]);
}
void PushR(int x){
    sum+=SUM(a[x]);
    ADD(a[x],1);
}
void PopR(int x){
    ADD(a[x],-1);
    sum-=SUM(a[x]);
}
ll CALC(int L,int R){
//  printf("BEF:%d\n",sum);
    while(LL>L)PushL(--LL);
    while(RR<R)PushR(++RR);
//  printf("BEF:%d\n",sum);
    while(LL<L)PopL(LL++);
    while(RR>R)PopR(RR--);
//  printf("[%d,%d]:%d\n",L,R,sum);
    return sum;
}
void solve(int l,int r,int L,int R){
    if(l>r)return;
//  printf("[%d,%d]:[%d,%d]\n",l,r,L,R);
    int mid=(l+r)>>1,MID=L;
    ll mn=CALC(L+1,mid)+f[L];
    for(int i=L+1;i<=R;i++)if(CALC(i+1,mid)+f[i]<mn)mn=CALC(i+1,mid)+f[i],MID=i;
    f[mid]=min(f[mid],mn+m);
    solve(l,mid-1,L,MID),solve(mid+1,r,MID,R);
}
void CDQ(int l,int r){
    if(l==r)return;
    int mid=(l+r)>>1;
    CDQ(l,mid);
    solve(mid+1,r,l,mid);
    CDQ(mid+1,r); 
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),f[i]=CALC(1,i)+m;
//  for(int i=1;i<=n;i++)printf("%d ",f[i]);puts("");
    CDQ(1,n);
//  for(int i=1;i<=n;i++)printf("%d ",f[i]);puts("");
    printf("%lld\n",f[n]);
    return 0;
}

CXXXI.[GYM102331J]Jiry Matchings

首先,不难想到一个 O(n^2) 的树上背包:设 f_{i,0/1,j} 表示在以 i 为根的子树内,其中 i 没有被匹配/被匹配了,且整个子树中共匹配了 j 条边的最优方案。考虑优化。

我们知道一个结论——对于任意带权网络流图,随着总流量的增加,最大费用是凸函数,即差分不增的函数。这可以被这样证明:我们在 i-1 点流量的残量网络上多找到一条增广路,显然这条路一定与之前所有增广路相容。则其长度一定不大于 i-1 时的任一增广路,不然完全可以把那条短的增广路替换掉。

显然,最大带权匹配也是带权网络流模型,也因此 f_{i,0/1} 这两个函数理论上都是凸的。但实际上,因为 f_{i,1} 强制 i 被匹配上,故不一定最优;但我们如果修改 f_{i,1} 的定义为 i 可以被匹配,也可以不被匹配,则此时显然两个函数都是凸的。

我们考虑合并一条边连接着的两个子树(也即一对父子),不妨设父亲为 u,儿子为 v,则 f_{u,0}\times f_{v,0}\rightarrow f_{u,0},\max\Big(f_{u,0}\times e(u,v)\times f_{v,0},f_{u,1}\times f_{v,1}\Big)\rightarrow f_{u,1}。其中,\times 符号为我们接下来定义的一种卷积,其可以作用于数组与数组间、数组与整数间;e(u,v) 为连接 (u,v) 的边的权值,对两个函数取 \max 意为对每一位分别取 \max

下面我们来考虑如何定义卷积。明显,数组间的卷积,不妨设为 f=g\times h,则有

f_i=\max\limits_{j+k=i}g_j+h_k

这个卷积对于普通的 g,h 函数是只有 O(n^2) 的计算方法的;但是,因为我们上文中出现的所有函数,按照我们之前的证明,都是的!我们再来看这个卷积,发现其本质是对于 g,h 在笛卡尔平面上形成的凸包的闵可夫斯基和,因为 g,h 均凸,所以是有 O(n) 的计算方法的!不考虑闵可夫斯基和的性质,我们也能得到,因为差分不增,所以若 f_i 从某一个 g_j+h_k 转移而来,则 f_{i+1} 一定是在 f_i 的基础上,加上 g_{j+1}-g_jh_{k+1}-h_k 二者中较大的一个得到的。贪心地处理,即可做到 O(n)

现在考虑数组与整数间卷积,明显就直接背包即可,也可 O(n) 直接得到。

于是我们合并一条边连接的子树的复杂度就是 O\Big(|u|+|v|\Big) 的,而非常规树上背包的 O\Big(|u||v|\Big),但是这样还是改变不了我们树上背包 O(n^2) 的事实。

下面考虑祭出一些奇奇怪怪的合并方式。考虑对这棵树重链剖分

现在,我们考虑先求出每个点仅考虑其所有轻儿子的子树时,其此时的 f_{i,0/1}

明显,对于点 u 来说,仅考虑其一个轻儿子 v 时,其转移上来的东西是 f_{v,1}\rightarrow f_{u,0},\max\Big(f_{v,1},f_{v,0}\times e(u,v)\Big)\rightarrow f_{u,1}

现在要求出其所有轻儿子转移上来的东西的卷积。如果直接按顺序一个一个乘的话,复杂度是 O(n^2) 的,因为每合并一个轻儿子,u 的函数大小就会增加,而我们卷积的复杂度是与 |u| 有关的,所用这样并不能保证复杂度。

但是,如果我们对其分治地进行合并的话,复杂度就是 O(n\log n) 的,因为每个轻儿子中所有元素都会被访问恰好 \log n 次。

显然,每个轻儿子仅会被转移一次,所以处理这个轻儿子的复杂度就是 O\Big(|u|\log n\Big) 的。因为关于树剖有一个结论,所有轻儿子的子树大小之和是 O(n\log n) 的,故此部分总复杂度是 O(n\log^2n) 的。

下面我们考虑重链部分的转移。显然,如果仍考虑归并地转移的话——显然,这里归并的状态就需要四个数组,即区间顶有/无被匹配、区间底有/无被匹配,且当归并到长度为 1 的区间时需要特殊考虑,因为此时区间的顶与底相同——复杂度仍为 O(n\log^2n),因为此时链上所有节点所代表的子树两两无交,故单次重链合并的复杂度就是 O\Big(|u|\log n\Big) 的,其中 u 是链顶节点。而每个链顶节点也同时是某个东西的轻儿子,故此部分复杂度仍是 O(n\log^2n) 的。

总复杂度 O(n\log^2n)。实际实现时可以用 vector 模拟每个函数。不必担心空间、时间等常数问题,因为我写的非常随便的代码都没卡常过去了,事实上大概也跑不满

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf=0x8080808080808080;
int n,head[200100],cnt;
struct node{int to,next,val;}edge[400100];
void ae(int u,int v,int w){
    edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
struct Array:vector<ll>{
    Array(){resize(1);}
    friend Array operator +(const Array &u,const Array &v){
        Array w;
        w.resize(u.size()+v.size()-1);
        for(int i=1,j=1,k=1;k<w.size();k++){
            if(i==u.size()){w[k]=w[k-1]+v[j]-v[j-1],j++;continue;}
            if(j==v.size()){w[k]=w[k-1]+u[i]-u[i-1],i++;continue;}
            if(u[i]-u[i-1]>v[j]-v[j-1])w[k]=w[k-1]+u[i]-u[i-1],i++;
            else w[k]=w[k-1]+v[j]-v[j-1],j++;
        }
        return w;
    }
    friend Array operator +(const Array &u,const int &v){
        Array w=u;w.push_back(inf);
        for(int i=1;i<w.size();i++)w[i]=max(w[i],u[i-1]+v);
        return w;
    }
    friend Array operator |(const Array &u,const Array &v){
        Array w;w.assign(max(u.size(),v.size()),inf);
        for(int i=0;i<w.size();i++){
            if(i<u.size())w[i]=max(w[i],u[i]);
            if(i<v.size())w[i]=max(w[i],v[i]);
        }
        return w;
    }
    void print()const{for(auto i:*this)printf("%d ",i);puts("");}
};
struct Paira{
    Array f,g;//f is the array for self matched-or-not, while g is the self not matched
    Paira(){f=Array(),g=Array();}
    Paira(Array F,Array G){f=F,g=G;}
    friend Paira operator +(const Paira &u,const Paira &v){
        Paira w;
        w.f=(u.f+v.g)|(u.g+v.f);
        w.g=u.g+v.g;
        w.f=w.f|w.g;
//      puts("U");u.print();
//      puts("V");v.print();
//      puts("W");w.print();
        return w;
    }
    void print()const{printf("[1]"),f.print();printf("[0]"),g.print();}
}a[200100];
struct Quada{
    Paira f,g;//f for upper matched-or-not, g for upper not matched
    Quada(){f=Paira(),g=Paira();}
    Quada(Paira F,Paira G){f=F,g=G;}
    friend Quada merge(const Quada &u,const int &v,const Quada &w,bool U,bool W){
        Quada r;
        r.f.f=(u.f.f+w.f.f)|((u.f.g+w.g.f)+v);
        r.f.g=(u.f.f+w.f.g);
        if(W)r.f.g=r.f.g|((u.f.g+w.g.g)+v);
        r.g.f=(u.g.f+w.f.f);
        if(U)r.g.f=r.g.f|((u.g.g+w.g.f)+v);
        r.g.g=(u.g.f+w.f.g);
        if(U&&W)r.g.g=r.g.g|((u.g.g+w.g.g)+v);
        r.f.g=r.f.g|r.g.g,r.g.f=r.g.f|r.g.g;
        r.f.f=r.f.f|r.f.g|r.g.f;
//      puts("U"),u.print();
//      puts("W"),w.print();
//      puts("R"),r.print();puts("");
        return r;
    }
    void print()const{puts("[[F]]"),f.print();puts("[[G]]"),g.print();} 
};
int fa[200100],son[201000],top[200100],sz[200100],val[200100];
void dfs1(int x){
    sz[x]=1;
    for(int i=head[x],y;i!=-1;i=edge[i].next){
        if((y=edge[i].to)==fa[x])continue;
        fa[y]=x,val[y]=edge[i].val,dfs1(y),sz[x]+=sz[y];
        if(sz[y]>sz[son[x]])son[x]=y;
    }
}
vector<int>v;
Paira lightsonsolve(int l,int r){
    if(l==r-1)return Paira(a[v[l]].g+val[v[l]],a[v[l]].f);
    int mid=(l+r)>>1;
    return lightsonsolve(l,mid)+lightsonsolve(mid,r);
}
Quada heavysonsolve(int l,int r){
    if(l==r)return Quada(Paira(a[v[l]].f,a[v[l]].g),Paira(a[v[l]].g,a[v[l]].g));
    int mid=(l+r)>>1;
//  printf("(%d,%d]:(%d,%d]+(%d,%d]\n",l,r,l,mid,mid,r);
    return merge(heavysonsolve(l,mid),val[v[mid+1]],heavysonsolve(mid+1,r),l!=mid,mid+1!=r);
}
void dfs2(int x){
    if(!top[x])top[x]=x;
    if(son[x])top[son[x]]=top[x],dfs2(son[x]);
    for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa[x]&&edge[i].to!=son[x])dfs2(edge[i].to);
    v.clear();for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa[x]&&edge[i].to!=son[x])v.push_back(edge[i].to);
//  printf("LIT%d:",x);for(auto i:v)printf("%d ",i);puts("");
    if(!v.empty())a[x]=lightsonsolve(0,v.size());
//  a[x].print();puts("");
    if(top[x]!=x)return;
    v.clear();for(int i=x;i;i=son[i])v.push_back(i);
//  printf("CHA%d:",x);for(auto i:v)printf("%d ",i);puts("");
    Quada tmp=heavysonsolve(0,v.size()-1);
    a[x]=Paira(tmp.f.f,tmp.g.f);
}
int main(){
    scanf("%d",&n),memset(head,-1,sizeof(head));
    for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
    dfs1(1),dfs2(1);
//  for(int i=1;i<=n;i++)printf("%d %d %d %d %d\n",fa[i],son[i],top[i],sz[i],val[i]);
//  for(int i=1;i<=n;i++)printf("%d:%d\n",i,a[i].f.size());
    for(int i=1;i<n;i++)if(i<a[1].f.size())printf("%lld ",a[1].f[i]);else printf("? ");puts("");
    return 0;
} 

CXXXII.[GYM102268J]Jealous Split

wqs二分。

首先,先讲一下wqs二分的应用条件:

对于某个函数 f(x) 和一个特定的 x,要求出 f(x) 的值的复杂度是不可接受的;但是,若满足 f 是上凸/下凹的,且对于一个给定的 k,求出函数 f(x)+xk 的全局最大(也可能是小,视 f 的凹凸性而定)值及取得最值的位置的复杂度是可接受的,则此时就可以使用此种方式来求解。

具体来说,明显 f(x) 会在平面上构成一个凸壳;而对于一个给定的 k 求全局的最值,就相当于拿一条斜率为 -k 的直线去切凸壳,最值出现的位置就是切点的位置。

明显我们可以二分 k,即二分切线斜率。显然,通过二分,最后定会切到给定的 x 的位置,则此时我们就已经知道了 f(x)+xk 的值及 k 的值,倒着推一下就能推回 f(x) 出来。

但需要注意的是,对于某些 k,可能其与凸壳的切点不止一个,但因为其是凸的,故所有切点必定是一段区间。这时候就只需判断要求的 x 是在区间左边、内部或者右边即可。假如在区间内部,就可以直接返回了。

我们回到本题。

先从简单的情形想起:假如仅能切一刀,应该怎么切?

明显我们可以初始令最左方的元素单独成段,然后剩下元素再成一段。之后,不断向右移动切点,直到再往右移动会使得左段元素和大于右端元素和。这时,明显一定满足题目要求,因为当且仅当切点将要跨越的那个元素比当前的差要大才会停止移动,而这本身就表明已经有一个元素大于当前差的绝对值。

当能切更多刀时,明显我们可以通过逐步调整法使得每一刀都符合上述要求,也即解一定存在。

我们发现,这种情形下,对于任意实数 k>1,都应有所有(段内元素之和的 k 次幂)之和最小。取 k=2 会使计算简便。

于是我们现在就要找到一种分成 m 段且所有段的平方和最小。显然直接DP是 O(n^2) 的。

但是,我们发现平方和,随着段数的越分越多,肯定是递减的;不仅如此,其还是下凸的,因为否则我们一定可以更早地分开一段使答案更优。

考虑套上wqs二分。于是我们就变成了每切一段需要额外的代价 k,求平方和加上额外代价最小的分段方式。这是简单斜率优化,可以 O(n) 使用单调队列简单解决。

但是依据我们上文对wqs二分的分析,其好像并不能很好地求出方案来(因为不能保证切线刚好就切到我们需要的点上)。事实上,对于大多数的wqs二分题,我们都无法找到一种方案。

但是,注意到这里说的是大多数!事实上,对于本题这种“区间划分”wqs二分问题,的确是有一种构造方案的。具体而言,当求出的区间包含目标位置时,我们可以求出分段最少的方法以及最多的方法——可以通过斜率优化时当队首首个和次个元素的斜率与所需斜率相等时,出不出队来分别求出。明显我们的目标介于两种方法之间。

考虑用一种方法的一段前缀与另一种方法的一段后缀拼成完整的分段方案。考虑如何拼接。

首先,若两者方法出现了相同的分割点,明显在分割点处拼接前一半和后一半是一种合法的方案,因为最小方法和最大方法本质相当于从初始状态到终止状态的两条不同的最短路,一条从起点到终点的最短路,对于路径上所有节点,也必是最短路,故拼接合法。

我们还有一种拼接方法。考虑最少方案中,出现了一段区间 [l,r];最多方案中,出现了一段区间 [L,R];若有 l<L\land R<r,则我们可以用最少方案中 l 以前的前缀,拼上最多方案中 R 以后的后缀构成一组方案,也可以用 L 以前和 r 以后拼成另一种方案。具体的话,因为区间和的平方函数满足四边形不等式,所以新方案一定不更劣;但因为其本身都是最短路,故新方案最优也只能是最短路,所以两者结合,就得到新方案必定是最短路。

可以被证明的是,一定存在一种拼接方案满足长度恰好为任何 k\in[L,R],其中 L,R 分别为最少方案、最多方案的长度。因为依据鸽巢原理,最多方案中一定至少有 R-L 个区间是最少方案中某个区间的子区间;而我们构造的一种方案中包含多少个上述区间,其长度就会在 L 基础上增加多少。明显我们上述方案中包含了从 0 个到 R-L 个所有可能需要的个数的构造方式,故我们一定可以构造出符合条件的方式。

于是我们现在已经知道如何二分、如何构造方式了。是不是就结束了呢?

稍等!我们还没有讨论 0 的情形——0 会使得问题变麻烦,因为斜率优化时前缀和就不是严格单调递增了。一个明智的想法是忽略所有 0,直到方式构造完成后再加入 0

然后就是一些具体实现的问题了。一开始我wqs二分写的是实数二分,因为考虑到斜率可能为一切实数。但是后来思考发现,只需整数二分,得到的直线就可以切到所有点,于是就换成了整数二分。

实际上,还有一个原因是本题数据范围就非常尴尬(序列中所有数之和最大为 50 亿,刚好爆 int,平方后就爆了 long long),即使使用long double,也会被卡精度 WA 71。用实数二分就无法避免地用 long double 存状态,因此会挂。

但是爆 long long 也意为着我们不能使用 long long 储存。我尝试使用了压 13bit8int 来模拟 __int128,但是被卡常了。所以最后索性直接上了 __int128(第一次知道 CF 开 C++17(64) 就能用 __int128 的),才卡过。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef __int128 ii;
int n,m,F[100100],G[100100];
ii f[100100],g[100100];
ll s[100100];
ii sqr(ll x){return (ii)x*x;}
int read(){
    int x=0;
    char c=getchar();
    while(c>'9'||c<'0')c=getchar();
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x;
}
int q[100100],l,r;
int che(ll ip){
//  printf("%lld:\n",ip);
    l=r=0;
    for(int i=1;i<=n;i++){
        if(s[i]==s[i-1]){f[i]=f[i-1],F[i]=F[i-1];continue;}
//      if(r-l>=1)(2*BigNum(s[i])*(s[q[l+1]]-s[q[l]])).print(),(f[q[l+1]]-f[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])).print(),puts("");
        while(r-l>=1&&f[q[l+1]]-f[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])<(ii)2*s[i]*(s[q[l+1]]-s[q[l]]))l++;
        F[i]=q[l],f[i]=f[q[l]]+sqr(s[i]-s[q[l]])+ip;
        while(r-l>=1&&(f[q[r]]-f[q[r-1]]+sqr(s[q[r]])-sqr(s[q[r-1]]))*(s[i]-s[q[r]])>(f[i]-f[q[r]]+sqr(s[i])-sqr(s[q[r]]))*(s[q[r]]-s[q[r-1]]))r--;
        q[++r]=i;
    }
//  for(int i=1;i<=n;i++)f[i].print();puts("");
//  for(int i=1;i<=n;i++)printf("%d ",F[i]);puts("");
    l=r=0;
    for(int i=1;i<=n;i++){
        if(s[i]==s[i-1]){g[i]=g[i-1],G[i]=G[i-1];continue;}
//      if(r-l>=1)printf("%d:%d,%d\n",i,q[l],q[l+1]),(2*BigNum(s[i])*(s[q[l+1]]-s[q[l]])).print(),(g[q[l+1]]-g[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])).print(),puts("");
        while(r-l>=1&&g[q[l+1]]-g[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])<=(ii)2*s[i]*(s[q[l+1]]-s[q[l]]))l++;
        G[i]=q[l],g[i]=g[q[l]]+sqr(s[i]-s[q[l]])+ip;
        while(r-l>=1&&(g[q[r]]-g[q[r-1]]+sqr(s[q[r]])-sqr(s[q[r-1]]))*(s[i]-s[q[r]])>=(g[i]-g[q[r]]+sqr(s[i])-sqr(s[q[r]]))*(s[q[r]]-s[q[r-1]]))r--;
        q[++r]=i;
    }
//  for(int i=1;i<=n;i++)g[i].print();puts("");
//  for(int i=1;i<=n;i++)printf("%d ",G[i]);puts("");
    l=0;for(int i=n;i;i=F[i])l++;
    r=0;for(int i=n;i;i=G[i])r++;
//  printf("[%d %d]\n",l,r);
    if(l>m)return -1;
    if(r<m)return 1;
    return 0;
}
vector<int>u,v;
int tot;
int main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++)s[i]=s[i-1]+read(),tot+=(s[i]!=s[i-1]);
    puts("Yes");
    if(tot<m){//cases when there have to be single 0 sections
        for(int i=1;i<n;i++){
            if(tot<m&&s[i]==s[i-1])tot++,printf("%d ",i);
            if(s[i]!=s[i-1])printf("%d ",i);
        }puts("");return 0;
    }
//  for(int i=1;i<=n;i++)printf("%Lf ",s[i]);puts("");
    ll L=0,R=0x3f3f3f3f3f3f3f3f,mid;
    while(true){
//      printf("%lld %lld\n",L,R);
        int tmp=che(mid=(L+R)>>1);
        if(tmp==-1)L=mid+1;
        if(tmp==1)R=mid-1;
        if(!tmp)break;
    }
//  printf("%d %d\n",l,r);
    for(int i=n;i;i=F[i])u.push_back(i);u.push_back(0),reverse(u.begin(),u.end());
    for(int i=n;i;i=G[i])v.push_back(i);v.push_back(0),reverse(v.begin(),v.end());
//  for(auto i:u)printf("%d ",i);puts("");
//  for(auto i:v)printf("%d ",i);puts("");
//  if(l==m){for(int i=1;i+1<u.size();i++)printf("%d ",u[i]);puts("");return 0;}
//  if(r==m){for(int i=1;i+1<v.size();i++)printf("%d ",v[i]);puts("");return 0;}
    for(int i=1,j=0;i<u.size();i++){
        for(;j<v.size()&&v[j]<u[i];j++){
            if(!j||v[j-1]<=u[i-1])continue;
            if(i+v.size()-j-1==m){for(int k=1;k<i;k++)printf("%d ",u[k]);for(int k=j;k+1<v.size();k++)printf("%d ",v[k]);puts("");return 0;}
            if(j+u.size()-i-1==m){for(int k=1;k<j;k++)printf("%d ",v[k]);for(int k=i;k+1<u.size();k++)printf("%d ",u[k]);puts("");return 0;}
        }
        if(j==v.size()||v[j]!=u[i])continue;
        if(i+v.size()-j-1==m){for(int k=1;k<i;k++)printf("%d ",u[k]);for(int k=j;k+1<v.size();k++)printf("%d ",v[k]);puts("");return 0;}
        if(j+u.size()-i-1==m){for(int k=1;k<j;k++)printf("%d ",v[k]);for(int k=i;k+1<u.size();k++)printf("%d ",u[k]);puts("");return 0;}
    }
    return 0;
}

CXXXIII.[HDU6094]Rikka with K-Match

依旧wqs二分。

首先,依据我们之前提到过的一个性质,“凡是可以表示成费用流模型的东西都有凹凸性”,本题也不例外,关于匹配个数的函数是凹的。

凹的就可以wqs二分。于是问题转换为最小权任意匹配。因为 m 只有 4,使用轮廓线DP即可在 O(nm2^m) 时间内解决问题。

需要注意的是本题二分的边界。或许会认为仅仅是 10^9 就行了,但考虑一条长度为 nm110^9 交错的链,且链的开头结尾都是 10^9,就会发现最后一条边的斜率是 10^9\times nm 级别的。于是要把边界开到 10^{14},这样能保证不爆 long long,同时还能求出答案。

时间复杂度 O(nm2^m\log 10^{14})

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T,n,m,q,X[40100][4],Y[40100][4],F[40100][4][1<<4],G[40100][4][1<<4],lim;
ll f[40100][4][1<<4];
void chmn(int i,int j,int k,int K,ll bon,bool mat){
    int I=i-!j,J=(!j?m:j)-1;
    if(f[i][j][k]>f[I][J][K]+bon)f[i][j][k]=f[I][J][K]+bon,F[i][j][k]=F[I][J][K]+mat,G[i][j][k]=G[I][J][K]+mat;
    else if(f[i][j][k]==f[I][J][K]+bon)F[i][j][k]=min(F[i][j][k],F[I][J][K]+mat),G[i][j][k]=max(G[i][j][k],G[I][J][K]+mat);
}
int che(ll ip){
//  printf("%d\n",ip);
    for(int i=0;i<n;i++)for(int j=0;j<m;j++){
        if(!i&&!j){for(int k=0;k<lim;k++)f[i][j][k]=0;continue;}
        for(int k=0;k<lim;k++)f[i][j][k]=0x3f3f3f3f3f3f3f3f;
        for(int k=0;k<lim;k++){
            if(i&&!(k&(1<<j)))chmn(i,j,k|(1<<j),k,X[i][j]-ip,true);
            if(j&&!(k&(1<<(j-1))))chmn(i,j,k|(1<<(j-1))|(1<<j),k,Y[i][j]-ip,true);
            chmn(i,j,k&(lim-1-(1<<j)),k,0,false);
        }
    }
    ll mn=0x3f3f3f3f3f3f3f3f;
    for(int k=0;k<lim;k++)mn=min(mn,f[n-1][m-1][k]);
    int L=0x3f3f3f3f,R=0;
    for(int k=0;k<lim;k++)if(mn==f[n-1][m-1][k])L=min(L,F[n-1][m-1][k]),R=max(R,G[n-1][m-1][k]);
    if(L>q)return -1;
    if(R<q)return 1;
    printf("%lld\n",mn+ip*q);
    return 0;
}
void read(int &x){
    x=0;
    char c=getchar();
    while(c>'9'||c<'0')c=getchar();
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
int main(){
    read(T);
    while(T--){
        read(n),read(m),read(q),lim=1<<m;
        for(int i=1;i<n;i++)for(int j=0;j<m;j++)read(X[i][j]);
        for(int i=0;i<n;i++)for(int j=1;j<m;j++)read(Y[i][j]);
        ll l=0,r=1e14,mid;
        while(true){
            int tp=che(mid=(l+r)>>1);
            if(tp==-1)r=mid-1;
            if(tp==1)l=mid+1;
            if(!tp)break;
        }
    }
    return 0;
}

CXXXIV.[BZOJ3864]Hero meet devil

我们不妨从最trival的LCS问题上想起:暴力的LCS求法是什么?

f(i,j) 表示一个串(不妨设为本题中要填的字符串 T)的前 i 位与另一个串(即题目中给出的 S)的前 j 位所构成的串的LCS。则 f(i,j)=\max\Big\{f(i,j-1),f(i-1,j),f(i-1,j-1)+[t_i=s_j]\Big\}

因为 |S| 很小,所以我们可以考虑设 f(i,\mathbb{S}) 表示当前填到 T 的第 i 位,且 \mathbb{S} 中通过某种方式储存了 f(i,0)\sim f(i,|S|) 全部的DP值,满足此种情形的串的方案数。下面考虑怎么搞出 \mathbb{S}

明显,对于 f(i,0\sim|S|) 来说,相邻两个数的差至多为 1,这一点可以直接从DP式上看出。于是我们可以状压其差分数组,即可实现由数组到状态的一一映射。这之后,我们只需预处理出对于每个 \mathbb S 对应的状态,其之后添加 A,G,C,T 四个字符会分别到达哪个新状态即可。然后直接DP就行了。

这种手法被称作“DP on DP”,因为DP状态中储存了另一种更简单的DP的数组。可以发现,简单DP的数组压缩后得到了一张自动姬。

时间复杂度 O(m2^n)

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,n,m,f[2][1<<15],lim,t[20],a[20],b[20],res[20],trans[1<<15][4];
char s[20];
int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%s%d",s+1,&m),n=strlen(s+1),lim=1<<n;
        for(int i=1;i<=n;i++){
            if(s[i]=='A')t[i]=0;
            if(s[i]=='G')t[i]=1;
            if(s[i]=='C')t[i]=2;
            if(s[i]=='T')t[i]=3;
        }
        for(int j=0;j<lim;j++){
            for(int k=0;k<n;k++)a[k+1]=a[k]+((j>>k)&1);
            for(int c=0;c<4;c++){
                for(int k=1;k<=n;k++)b[k]=max({a[k-1]+(t[k]==c),a[k],b[k-1]});
                trans[j][c]=0;
                for(int k=0;k<n;k++)trans[j][c]|=(b[k+1]-b[k])<<k;
            }
        }
        memset(f,0,sizeof(f)),f[0][0]=1;
        for(int i=0;i<m;i++){
            memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
            for(int j=0;j<lim;j++)for(int k=0;k<4;k++)(f[!(i&1)][trans[j][k]]+=f[i&1][j])%=mod;
        }
//      for(int i=0;i<=m;i++){for(int j=0;j<lim;j++)printf("%d ",f[i][j]);puts("");}
        for(int i=0;i<lim;i++)(res[__builtin_popcount(i)]+=f[m&1][i])%=mod;
        for(int i=0;i<=n;i++)printf("%d\n",res[i]),res[i]=0;
    }
    return 0;
}

CXXXV.[ZOJ3989]Triangulation

神题。

这个数据范围很难不让人想到状压DP。于是我们考虑应该怎么设计状态。

考虑一组三角剖分的形态:其必定是在所有点所构成的凸包内部划分出很多三角形。这也就表明,任何一组三角剖分一定包含所有凸包上的边。

我们可以想到一个比较简洁的DP:设 f(\mathbb S) 表示在点集 \mathbb S 所构成的凸包内部的所有点的三角剖分。但是这样子跑的话,每一组三角剖分都会被统计多次,不能不重复地计数。

于是我们另辟蹊径。我们令初始三角剖分上的边仅包含所有点集的下凸壳上的所有边。之后,维护当前剖分的上边界,每次往上边界上添加一个新的三角形,得到新的上边界,这样持续不断直到填成了整个点集的上凸壳

但是,出于不重复计数的考虑,我们必须对该上边界做出限制。

我们强制该上边界上的点的 x 坐标严格递增。这样,只需知道点集就能唯一还原出边界,而不必存储点集内部的顺序了。

同时,为了处理有点 x 坐标相同的情形,我们将所有点随机旋转一个角度,这样便消除了 x 坐标相同的可能(如果仍相同,继续转即可)。于是以下默认所有点 x 坐标互不相同。

接着考虑如何添加三角形。

我们发现,其可以分为两种情形:

  1. 对于边界上连续的三个点 A,B,C,满足 BAC 下方,且 \triangle_{ABC} 内部无点,此时便可以连边 AC,将 B 从边界上删除。

  2. 对于边界上连续的两个点 A,B,存在一个 CAB 上方,且 Cx 坐标介于 A,B 之间,且 \triangle_{ABC} 内部无点,此时便可以连边 AC,BC,并将 C 插入上边界。

为了不重复计算,我们每次仅加入上边界上最左的三角形。也就是说,如果一条边 AB 在某一轮转移中没有生长出三角形来,则其之后也一定不会再长三角形。同时,对于情形1,我们将其算作右边的 BC 边长出的三角形,这样便统一了条件。

这样,我们便可以设 f(\mathbb S,i) 表示现在上边界是集合 \mathbb S,且 \mathbb S 中前 i 条边都已经无法再生长的状态。这时,考虑第 i+1 条边,处理其长或不长的状态即可。

需要注意的是,因为 x 坐标最大最小的两个点无论何时都必在上边界上,所以可以在状态中不记录它们,这样剩下的所有状态都是可能出现的状态了。且因为没有明确的转移顺序,采取bfs转移。

通过预处理一大坨东西(三角形内部有没有点啦、情形2的哪些 C 合法啦,之类的),我们可以做到 O(n^4+n2^n) 的复杂度,其中 n^4 是预处理。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T,n,ord[20],a[20][20],rev[20],f[1<<16][17],in[1<<16],b[20];
ll g[1<<16][17];
const double eps=1e-13;
const double pi=acos(-1);
int cmp(double x){
    if(x>eps)return 1;
    if(x<-eps)return -1;
    return 0;
}
struct Vector{
    double x,y;
    Vector(){}
    Vector(double X,double Y){x=X,y=Y;}
    friend Vector operator +(const Vector &u,const Vector &v){return Vector(u.x+v.x,u.y+v.y);}
    friend Vector operator -(const Vector &u,const Vector &v){return Vector(u.x-v.x,u.y-v.y);}
    friend Vector operator *(const Vector &u,const double &v){return Vector(u.x*v,u.y*v);}
    friend Vector operator /(const Vector &u,const double &v){return Vector(u.x/v,u.y/v);}
    friend double operator &(const Vector &u,const Vector &v){return u.x*v.y-u.y*v.x;}//cross times
    friend double operator |(const Vector &u,const Vector &v){return u.x*v.x+u.y*v.y;}//point times
    friend bool operator <(const Vector &u,const Vector &v){return u.x<v.x;}
    double operator ~()const{return sqrt(x*x+y*y);}//the modulo of a vector
    double operator !()const{return atan2(y,x);}//the angle of a vector
    void print(){printf("(%lf,%lf)",x,y);}
    void rotate(double ang){
        double modu=~*this;
        double angl=!*this;
        angl+=ang;
        x=cos(angl)*modu,y=sin(angl)*modu;
    }
}p[20];
typedef Vector Point;
struct Line{
    Point x,y;
    Vector z;
    Line(){}
    Line(Point X,Point Y){x=X,y=Y,z=Y-X;}
    friend Point operator &(const Line &u,const Line &v){return u.x+u.z*((v.z&(u.x-v.x))/(u.z&v.z));}
};
typedef Line Segment;
bool CMP(int u,int v){return p[u].x<p[v].x;}
bool lower[20],upper[20];
int stk[20],tp,LO,UP;
queue<int>q;
void trans(int i,int j,int I,int J,int k){
    if(!--in[I])q.push(I);
    if(f[I][J]>k)f[I][J]=k,g[I][J]=g[i][j];
    else if(f[I][J]==k)g[I][J]+=g[i][j];
}
vector<int>v[20][20];
bool abv[20][20][20],ins[20][20][20];//if above, true.
int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%d",&n),LO=UP=0,memset(f,0x3f,sizeof(f)),memset(in,0,sizeof(in)),memset(ins,true,sizeof(ins));
        for(int i=0;i<n;i++)scanf("%lf%lf",&p[i].x,&p[i].y),ord[i]=i,lower[i]=upper[i]=false;
        while(true){
            double ang=(1.0*rand()/RAND_MAX)*2*pi;
            for(int i=0;i<n;i++)p[i].rotate(ang);
            sort(ord,ord+n,CMP);
            bool ok=true;
            for(int i=1;i<n;i++)if(!cmp(p[i].x-p[i-1].x)){ok=false;break;}
            if(ok)break;
        }
        for(int i=0;i<n;i++)rev[ord[i]]=i;
        for(int i=0;i<n;i++)for(int j=0;j<n;j++)scanf("%d",&a[rev[i]][rev[j]]);
        sort(p,p+n);
//      for(int i=0;i<n;i++)p[i].print();puts("");

        for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)for(int k=i+1;k<j;k++)abv[i][j][k]=(cmp((p[i]-p[k])&(p[j]-p[k]))==1);

        stk[++tp]=0;
        for(int i=1;i<n;i++){
            while(tp>=2&&cmp((p[stk[tp-1]]-p[stk[tp]])&(p[i]-p[stk[tp]]))!=-1)tp--;
            stk[++tp]=i;
        }
        for(int i=1;i<=tp;i++)lower[stk[i]]=true,LO|=1<<stk[i];
        LO=(LO^(1<<(n-1)))>>1,f[LO][0]=0,g[LO][0]=1;
        for(int i=1;i<tp;i++)f[LO][0]+=a[stk[i]][stk[i+1]];
        tp=0;

        stk[++tp]=0;
        for(int i=1;i<n;i++){
            while(tp>=2&&cmp((p[stk[tp-1]]-p[stk[tp]])&(p[i]-p[stk[tp]]))!=1)tp--;
            stk[++tp]=i;
        }
        for(int i=1;i<=tp;i++)upper[stk[i]]=true,UP|=1<<stk[i];
        UP=(UP^(1<<(n-1)))>>1;
        tp=0;

//      printf("STA:%d %d\n",LO,UP);
//      for(int i=0;i<n;i++)printf("(%d %d)",lower[i],upper[i]);puts("");

        for(int i=0;i<n;i++)for(int j=i+1;j<n;j++){
            for(int k=i+1;k<j;k++){
                if(abv[i][j][k]){
                    bool ok=true;
                    for(int l=i+1;l<k;l++)if(abv[i][j][l]&&!abv[i][k][l]){ok=false;break;}
                    if(!ok)continue;
                    for(int l=k+1;l<j;l++)if(abv[i][j][l]&&!abv[k][j][l]){ok=false;break;}
                    if(!ok)continue;
                    v[i][j].push_back(k);                   
                }else{
                    ins[i][j][k]=false;
                    for(int l=i+1;l<k;l++)if(!abv[i][j][l]&&abv[i][k][l]){ins[i][j][k]=true;break;}
                    for(int l=k+1;l<j;l++)if(!abv[i][j][l]&&abv[k][j][l]){ins[i][j][k]=true;break;}
                }
            }
        }

        for(int i=0;i<(1<<(n-2));i++){
            int len=0;b[len++]=0;
            for(int j=0;j<n-2;j++)if(i&(1<<j))b[len++]=j+1;
            b[len++]=n-1;
//          printf("%d:",i);for(int j=0;j<len;j++)printf("%d ",b[j]);puts("");
            for(int j=0;j<len;j++){
                if(j&&j+1<len&&!ins[b[j-1]][b[j+1]][b[j]])in[i^(1<<(b[j]-1))]++;
                if(j)for(auto k:v[b[j-1]][b[j]])in[i|(1<<(k-1))]++;
            }
        }
        q.push(LO);
        while(!q.empty()){
            int i=q.front();q.pop();
//          printf("SS:%d\n",i);
            int len=0;b[len++]=0;
            for(int j=0;j<n-2;j++)if(i&(1<<j))b[len++]=j+1;
            b[len++]=n-1;
            for(int j=0;j<len;j++){
                if(j&&j+1<len&&!ins[b[j-1]][b[j+1]][b[j]])trans(i,j,i^(1<<(b[j]-1)),j-1,f[i][j]+a[b[j-1]][b[j+1]]);
                if(j)for(auto k:v[b[j-1]][b[j]])trans(i,j-1,i|(1<<(k-1)),j-1,f[i][j-1]+a[b[j-1]][k]+a[b[j]][k]);
                if(j+2<len)trans(i,j,i,j+1,f[i][j]);
            }
        }
        printf("%d %lld\n",f[UP][__builtin_popcount(UP)],g[UP][__builtin_popcount(UP)]);

        for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)v[i][j].clear();
    }
    return 0;
}

CXXXVI.[[IOI2000] 邮局 加强版]()

Observation 1. 若一段村庄中设一个邮局,则邮局一定设在其中位数(若是偶数则任一中位数)的位置。

Observation 2. 若令 w(l,r) 为区间 (l,r) 中村庄设一个邮局的费用,则其满足四边形不等式。

Observation 3. 显然全段中修几个邮局的函数有凹性,可以wqs二分。

考虑check二分的 mid。则我们有转移式

f_i=\min\limits_{j<i}f_j+w(j+1,i)+mid

当转移分层时,因为四边形不等式得出的决策单调性,可以分治解决;不分层时,一种解法是使用CDQ分治,正如CXXX.[GYM102904B]Dispatch Money。但是那题囿于逆序对没有 O(1) 的在线计算方法,所以只能莫队式解决;而本题的 w 函数是可以简单 O(1) 在线解决的,因此没有必要使用CDQ分治。

考虑我们当前已经求出了 1\sim i 所有的 f 值。此时,位置 0 转移的位置必定是从 i+1 开始的一段(当然可能为空),位置 1 转移的位置必定是紧接着 0 的转移段后的一段(仍可能为空)……位置 i 是紧接着 i-1 的转移段往后一直到结尾的一段。

显然,此时 i+1 最优转移点已经明确,是从左到右第一个非空的段。但是我们要让其它东西也可以从 i+1 转移来。

明显,i+1 转移到的位置必定是一段后缀。于是我们从右往左枚举每一段,若这段最左方的位置从 i+1 转移更优,显然这一整段都应从 i+1 转移,然后再判断下一段;否则,就在这段中二分出 i+1 更优的后缀,然后这段后缀,再加上之前已经判断为 i+1 转移的那些段,就是 i+1 的转移段。

这样,内层DP的复杂度就是 O(n\log n) 的;再套上wqs二分,复杂度就是 O(n\log^2n) 的。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[500100],F[500100],G[500100],pos[500100],trs[500100],l,r;
ll s[500100],f[500100],g[500100];
ll calc(int l,int r){return s[l-1]+s[r]-s[(l+r)/2]-s[(l+r-1)/2];}
void read(int &x){
    x=0;
    char c=getchar();
    while(c>'9'||c<'0')c=getchar();
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
}
int che(ll ip){
//  printf("%lld\n",ip); 
    l=r=1,trs[l]=0;
    for(int i=1;i<=n;i++){
        if(l<r&&pos[l]==i)l++;
        f[i]=f[trs[l]]+calc(trs[l]+1,i)+ip,F[i]=F[trs[l]]+1;
        if(i==n)break;
        pos[l-1]=i+1,pos[r]=n+1;
        while(true){
            if(r<l){trs[++r]=i;break;}
            if(f[i]+calc(i+1,pos[r-1])<=f[trs[r]]+calc(trs[r]+1,pos[r-1])){r--;continue;}
            int L=pos[r-1],R=pos[r];
            while(L+1<R){
                int mid=(L+R)>>1;
                if(f[i]+calc(i+1,mid)<=f[trs[r]]+calc(trs[r]+1,mid))R=mid;
                else L=mid;
            }
            if(R<=n)r++,pos[r-1]=R,trs[r]=i;
            break;
        }
    }
//  for(int i=1;i<=n;i++)printf("%lld ",f[i]);puts("");
//  for(int i=1;i<=n;i++)printf("%d ",F[i]);puts("");

    l=r=1,trs[l]=0;
    for(int i=1;i<=n;i++){
        if(l<r&&pos[l]==i)l++;
        g[i]=g[trs[l]]+calc(trs[l]+1,i)+ip,G[i]=G[trs[l]]+1;
        if(i==n)break;
        pos[l-1]=i+1,pos[r]=n+1;
        while(true){
            if(r<l){trs[++r]=i;break;}
            if(g[i]+calc(i+1,pos[r-1])<g[trs[r]]+calc(trs[r]+1,pos[r-1])){r--;continue;}
            int L=pos[r-1],R=pos[r];
            while(L+1<R){
                int mid=(L+R)>>1;
                if(g[i]+calc(i+1,mid)<g[trs[r]]+calc(trs[r]+1,mid))R=mid;
                else L=mid;
            }
            if(R<=n)r++,pos[r-1]=R,trs[r]=i;
            break;
        }
    }

    int R=F[n],L=G[n];
//  printf("%d %d\n",L,R);
    if(L>m)return 1;
    if(R<m)return -1;
    printf("%lld\n",f[n]-ip*m);
    return 0;
}
int main(){
    read(n),read(m);
    for(int i=1;i<=n;i++)read(a[i]);
    sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
    ll l=0,r=1e12,mid;
    while(true){
        int tp=che(mid=(l+r)>>1);
        if(tp==-1)r=mid-1;
        if(tp==1)l=mid+1;
        if(!tp)break;
    }
    return 0;
}

CXXXVII.[国家集训队]Tree I

两年前刚学MST时做这题WA了,然后两年后才把它补上……

明显直接wqs二分就行了。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,q,dsu[50100],ip;
int read(){
    int x=0;
    char c=getchar();
    while(c>'9'||c<'0')c=getchar();
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x;
}
struct dat{
    int u,v,w,c;
    void READ(){u=read()+1,v=read()+1,w=read(),c=!read();}
    int calc()const{return c?ip+w:w;}
    friend bool operator<(const dat&u,const dat&v){return u.calc()==v.calc()?u.c<v.c:u.calc()<v.calc();}
}p[100100];
vector<dat>v[2];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
bool merge(int x,int y){x=find(x),y=find(y);if(x==y)return false;dsu[x]=y;return true;}
int che(){
//  printf("%lld:\n",ip);
    for(int i=0,j=0;i<v[0].size()||j<v[1].size();)if(i!=v[0].size()&&(j==v[1].size()||v[0][i]<v[1][j]))p[i+j+1]=v[0][i],i++;else p[i+j+1]=v[1][j],j++;
    int f=0,F=0;
    for(int i=1;i<=n;i++)dsu[i]=i;
    for(int i=1,j=1;i<=m;i=j){
        while(j<=m&&p[i].calc()==p[j].calc())j++;
        for(int k=i;k<j;k++)if(merge(p[k].u,p[k].v))f+=p[k].calc(),F+=p[k].c;
    }

    int g=0,G=0;
    for(int i=1;i<=n;i++)dsu[i]=i;
    for(int i=1,j=1;i<=m;i=j){
        while(j<=m&&p[i].calc()==p[j].calc())j++;
        for(int k=j-1;k>=i;k--)if(merge(p[k].u,p[k].v))g+=p[k].calc(),G+=p[k].c;
    }
    if(F>q)return 1;
    if(G<q)return -1;
    printf("%d\n",f-ip*q);
    return 0;
}
int main(){
//  freopen("P2619_10.in","r",stdin);
    n=read(),m=read(),q=read();
    for(int i=1;i<=m;i++)p[i].READ(),v[p[i].c].push_back(p[i]);
    for(int i=0;i<2;i++)sort(v[i].begin(),v[i].end());
    int l=-100,r=100;
    while(true){
        ip=(l+r)>>1;
        int tp=che();
        if(tp==-1)r=ip-1;
        if(tp==1)l=ip+1;
        if(!tp)return 0;
    }
    return 0;
} 

CXXXVIII.CF739E Gosha is hunting

因为 n2000,我们可以想出设一个二维状态来保证 n,a 这两维,然后再wqs二分 b 这一维。(明显,随着 b 越来越大,呈现一凸函数)。

时间复杂度 O(n^2\log n)

需要注意的是,在wqs二分 b 后,我们不能再二分 a,因为我们不知道此时应该优先最小化 a 的数量还是 b 的数量。

代码:

#include<bits/stdc++.h>
using namespace std;
const double eps=1e-9;
int n,a,b,F[2010][2010],G[2010][2010];
double p[2010],q[2010],f[2010][2010];
int cmp(double ip){
    if(ip>eps)return 1;
    if(ip<-eps)return -1;
    return 0;
}
void trans(int i,int j,int I,int J,double k,int K){
    if(cmp(f[I][J]-k)==-1)f[I][J]=k,F[I][J]=F[i][j]+K,G[I][J]=G[i][j]+K;
    else if(cmp(f[I][J]-k)==0)F[I][J]=min(F[I][J],F[i][j]+K),G[I][J]=max(G[I][J],G[i][j]+K);
}
int che(double ip){
    for(int i=1;i<=n;i++)for(int j=0;j<=min(i,a);j++)f[i][j]=-n;
    for(int i=0;i<n;i++)for(int j=0;j<=min(i,a);j++){
        if(j+1<=a){
            trans(i,j,i+1,j+1,f[i][j]+p[i+1],0);
            trans(i,j,i+1,j+1,f[i][j]+1-(1-p[i+1])*(1-q[i+1])-ip,1);
        }
        trans(i,j,i+1,j,f[i][j],0);
        trans(i,j,i+1,j,f[i][j]+q[i+1]-ip,1);
    }
    if(F[n][a]>b)return 1;
    if(G[n][a]<b)return -1;
    printf("%lf\n",f[n][a]+ip*b);
    return 0;
}
int main(){
    scanf("%d%d%d",&n,&a,&b);
    for(int i=1;i<=n;i++)scanf("%lf",&p[i]);
    for(int i=1;i<=n;i++)scanf("%lf",&q[i]);
    double l=0,r=1,mid;
    while(true){
        int tp=che(mid=(l+r)/2);
        if(tp==1)l=mid;
        if(tp==-1)r=mid;
        if(!tp)break;
    }
    return 0;
}

CXXXIX.[AGC030F] Permutation and Minimum

看到 300 的数据范围,再加上计数题,很容易就往计数DP方向去想。

为方便,我们将 n 乘二。

因为是两个位置取 \min,于是我们便想到从小往大把每个数填入序列。于是DP数组第一维的意义便出来了:当前已经填入了前 i 小个数。

考虑当前填入一个数。这个数有两种可能:一是与比它小的数匹配——此时最小值已经确定了,是那个更小的数,而更小数已经被填入序列,所以当前数与哪个比它小的数匹配根本不影响。二是与比它大的数匹配——此时最小值不确定,因此与不同的比它大的数在不同位置匹配会有不同结果。

于是我们便依次设出了剩余两维的意义:前 i 位中有 j 个东西是确定的(指与它配对的东西以及这一对数所在的位置都被确定,不论这确定的对是后来填出的还是初始序列中就已经给出的;若初始序列中只给出了一半的数,不算作此类),k 个东西是固定的(指其所在的位置固定,但与其配对的东西尚未被确定,显然此种情形下与其匹配的东西应是一个未在原序列中出现的东西),则剩余 i-j-k 个东西匹配了原序列中出现的东西,且该另一半必比 i 大。(注意这里我们把确定固定这两个词黑体了,因为我们接下来还要多次使用它们)

我们考虑分情况从 f_{i,j,k} 转移到 f_{i+1,j',k'}

情形1. 若数 i+1 在序列中被给出了:

情形1.1. 若数 i+1 在序列中被给出了,且其配对也被给出了:

则此时显然其已被唯一确定,直接划归 j 类。故此种情形唯一可行转移是 f_{i,j,k}\rightarrow f_{i+1,j+1,k}

情形1.2. 若数 i+1 在序列中被给出,但其配对未被给出:

有两种情形:其与比它小的东西匹配,或者与比它大的东西匹配。

情形1.2.1. 与比它小的东西匹配。

则依照定义,其应与 i-j-k 中某个东西匹配,且与其中不同东西匹配有影响(因为匹配的另一半是此段的最小值)。匹配完后 j 类多出了两个。则转移是 f_{i,j,k}\times(i-j-k)\rightarrow f_{i+1,j+2,k}

情形1.2.2. 与比它大的东西匹配。

则依照定义,其应划归 k 类,留待以后处理。故 f_{i,j,k}\rightarrow f_{i+1,j,k+1}

情形2. 若数 i+1 在序列中未被给出:

情形2.1. 作为 k 类中某个东西的另一半。

则此时与 k 类中哪个东西匹配根本不影响,因为每一对的最小值已经被确定了。因此若 k 非零,则有 f_{i,j,k}\rightarrow f_{i,j+2,k-1}

情形2.2. 作为 i-j-k 类。

则直接划归即可,因为方案数已在1.2.1中被计算。故 f_{i,j,k}\rightarrow f_{i+1,j,k}

情形2.3. 作为 k 类。

依照定义,k 类中元素的位置都是固定的。故 i+1 应被填到一个空余区间里。考虑计算此种空余区间的数量。

显然,总序列中,若我们设 sur_i 表示填入前 i 个数后序列中剩余的确定的数(即原序列中给出的已经确定好的数对),则目前一共有 j+sur_i 个数已经确定。k 类中已有的东西每个都占掉了 2 个格子,故还得减去 2k;再令 sig_i 表示填入前 i 个数后剩余的固定的数(即原序列中给出的确定好一半的数对),显然其也各占去 2 个格子;又因为同一个区间里两个数的顺序无影响,所以还得除以 2。所以我们最终得到了空余区间的数量为 \dfrac{n-(j+sur_i)-2k-2sig_i}{2}。若设其为 spr,则有 f_{i,j,k}\times spr\rightarrow f_{i+1,j,k+1}

显然转移 O(1);于是复杂度 O(n^3) 解决。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,a[610],p[610],f[2][610][610],sig[610],mat[610],sur[610];
int main(){
    scanf("%d",&n),n<<=1;
    for(int i=1;i<=n;i++)mat[i]=((i-1)^1)+1;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),p[a[i]]=i;

    for(int i=1;i<=n;i++)if(a[i]!=-1&&a[mat[i]]==-1)sig[a[i]-1]++;
    for(int i=n;i>=0;i--)sig[i]+=sig[i+1];

    for(int i=1;i<=n;i++)if(a[i]!=-1&&a[mat[i]]!=-1)sur[a[i]-1]++;
    for(int i=n;i>=0;i--)sur[i]+=sur[i+1];

//  for(int i=1;i<=n;i++)printf("%d ",sur[i]);puts(""); 
//  for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
//  for(int i=1;i<=n;i++)printf("%d ",p[i]);puts("");
    f[0][0][0]=1;
    for(int i=0;i<n;i++){
        memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
        for(int j=0;j<=i;j++)for(int k=0;j+k<=i;k++){
            if(n-(j+sur[i])-k*2-sig[i]*2<0)continue;
            if(!f[i&1][j][k])continue;
//          if(j+k!=i)continue;
//          printf("%d %d %d:%d\n",i,j,k,f[i&1][j][k]);
            if(p[i+1]){
                if(a[mat[p[i+1]]]!=-1){f[!(i&1)][j+1][k]=f[i&1][j][k];continue;}
                (f[!(i&1)][j][k+1]+=f[i&1][j][k])%=mod;//we left i+1 unmatched, which should be considered as k.
//              printf("I-J-K:%d\n",i-j-k);
                (f[!(i&1)][j+2][k]+=1ll*(i-j-k)*f[i&1][j][k]%mod)%=mod;//we match something unsure position
            }else{
                if(k)(f[!(i&1)][j+2][k-1]+=f[i&1][j][k])%=mod;//we match something of sure position
                (f[!(i&1)][j][k]+=f[i&1][j][k])%=mod;//match it with an element in [i+2,n]
                (f[!(i&1)][j][k+1]+=1ll*(n-(j+sur[i])-k*2-sig[i]*2)/2*f[i&1][j][k]%mod)%=mod;//put it in any empty space.
            }
        }   
    }
    printf("%d\n",f[n&1][n][0]);
    return 0;
}

CXL.忘情

wqs二分水题,明显那个式子可以被化成 (1+\sum x)^2,于是可以斜率优化,然后又明显随着段数越分越多函数是凹的,于是可以简单wqs二分。时间复杂度 O(n\log n)

需要注意的是,因为二分上界是 10^{18},所以得开 __int128

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef __int128 ii;
int n,m,s[100100],F[100100],q[100100],l,r;
ii f[100100];
inline void print(ii x){
    if(x<=9)putchar('0'+x);
    else print(x/10),putchar('0'+x%10);
}
ll sqr(int x){return 1ll*x*x;}
int che(ll ip){
//  printf("%lld\n",ip);
    l=r=0;
    for(int i=1;i<=n;i++){
        while(l<r&&f[q[l]]-f[q[l+1]]+sqr(s[q[l]])-sqr(s[q[l+1]])-2*(s[q[l]]-s[q[l+1]])>2ll*s[i]*(s[q[l]]-s[q[l+1]]))l++;
//      printf("%d:%d\n",i,q[l]);
        f[i]=f[q[l]]+sqr(s[i]-s[q[l]]+1)+ip,F[i]=F[q[l]]+1;
        while(l<r&&(f[q[r-1]]-f[q[r]]+sqr(s[q[r-1]])-sqr(s[q[r]]))*(s[q[r]]-s[i])>(f[q[r]]-f[i]+sqr(s[q[r]])-sqr(s[i]))*(s[q[r-1]]-s[q[r]]))r--;
        q[++r]=i;
    }
//  for(int i=1;i<=n;i++)printf("%lld ",f[i]);puts("");
//  for(int i=1;i<=n;i++)printf("%d ",F[i]);puts("");
    int L=F[n];
    l=r=0;
    for(int i=1;i<=n;i++){
        while(l<r&&f[q[l]]-f[q[l+1]]+sqr(s[q[l]])-sqr(s[q[l+1]])-2*(s[q[l]]-s[q[l+1]])>=2ll*s[i]*(s[q[l]]-s[q[l+1]]))l++;
        f[i]=f[q[l]]+sqr(s[i]-s[q[l]]+1)+ip,F[i]=F[q[l]]+1;
        while(l<r&&(f[q[r-1]]-f[q[r]]+sqr(s[q[r-1]])-sqr(s[q[r]]))*(s[q[r]]-s[i])>=(f[q[r]]-f[i]+sqr(s[q[r]])-sqr(s[i]))*(s[q[r-1]]-s[q[r]]))r--;
        q[++r]=i;
    }
    int R=F[n];
//  printf("[%d,%d]\n",L,R);
    if(L>m)return 1;
    if(R<m)return -1;
    print(f[n]-(ii)ip*m);
    return 0;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&s[i]),s[i]+=s[i-1];
    ll L=0,R=1e18,mid;
    while(true){
        int tp=che(mid=(L+R)>>1);
        if(tp==-1)R=mid-1;
        if(tp==1)L=mid+1;
        if(!tp)break;
    }
    return 0;
}

CXLI.[八省联考2018]林克卡特树

一眼发现函数是凸的。然后思考发现直接一个树形DP就能进行二分的check:设 f_{i,0/1/2} 分别表示节点 i,其中 i 未被选/是一条链的链顶/被一条链经过,然后直接DP就行。

为什么二分边界要开到 10^{12}

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,g[300100][3],head[300100],cnt;
ll f[300100][3],ip;//0:x is on nothing 1:x is currently on a path 2:x's path has been matched
struct node{int to,next,val;}edge[600100];
void ae(int u,int v,int w){
    edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
void trans(ll &F,int &G,ll ff,int gg){
    if(F<ff)F=ff,G=gg;
    else if(F==ff)G=min(G,gg);
}
void dfs(int x,int fa){
    f[x][0]=f[x][1]=f[x][2]=0,g[x][0]=g[x][1]=g[x][2]=0;
    for(int i=head[x],y,z;i!=-1;i=edge[i].next){
        y=edge[i].to,z=edge[i].val;
        if(y==fa)continue;
        dfs(y,x);

        f[x][2]+=f[y][2],g[x][2]+=g[y][2];
//      printf("%d,%d:%d,%d,%d,%d\n",x,y,f[x][1],f[y][1],z,-ip);
        trans(f[x][2],g[x][2],f[x][1]+f[y][1]+z-ip,g[x][1]+g[y][1]+1);

        f[x][1]+=f[y][2],g[x][1]+=g[y][2];

        trans(f[x][1],g[x][1],f[x][0]+f[y][1]+z,g[x][0]+g[y][1]);

        f[x][0]+=f[y][2],g[x][0]+=g[y][2];
    }
    trans(f[x][2],g[x][2],f[x][1]-ip,g[x][1]+1);
    trans(f[x][2],g[x][2],f[x][0],g[x][0]);
//  printf("%d:\n%d %d %d\n%d %d %d\n",x,f[x][0],f[x][1],f[x][2],g[x][0],g[x][1],g[x][2]);
}
int main(){
    scanf("%d%d",&n,&m),m++,memset(head,-1,sizeof(head));
    for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
    ll l=-1e12,r=1e12;
    while(l<r){
        ip=(l+r)>>1;
//      printf("%d------------\n",ip); 
        dfs(1,0);
        if(g[1][2]<=m)r=ip;
        else l=ip+1;
    }
    ip=l,dfs(1,0);
    printf("%lld\n",f[1][2]+1ll*l*m);
    return 0;
}

CXLII.CF1158F Density of subarrays

题解

CXLIII.[AGC013E] Placing Squares

关键是将问题从抽象的“正方形面积”转为具象的形式:一段长度为 d 的区间,有两个不同的小球要放进去,则总放置方案就是 d^2,且不同的区间间方案是通过乘法原理结合的,刚好是题目中 \prod d^2 的形式。

于是我们可以设计DP:设 f_{i,j} 表示前 i 个格子中,最后一段中有 j 个球。明显 j\in[0,2]

常规的位置可以直接矩阵快速幂解决;然而对于题目中给出的特殊位置,需要用特殊的矩阵处理。

因此复杂度 O(3^3m\log n)

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,a[100100];
const int mod=1e9+7;
struct Matrix{
    int g[3][3];
    int*operator[](int x){return g[x];}
    Matrix(){memset(g,0,sizeof(g));}
    friend Matrix operator*(Matrix x,Matrix y){
        Matrix z;
        for(int i=0;i<3;i++)for(int j=0;j<3;j++)for(int k=0;k<3;k++)(z[i][j]+=1ll*x[i][k]*y[k][j]%mod)%=mod; 
        return z;
    }
}A,B,R;
Matrix ksm(Matrix x,int y){
    Matrix z;
    z[0][0]=z[1][1]=z[2][2]=1;
    for(;y;y>>=1,x=x*x)if(y&1)z=z*x;
    return z; 
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)scanf("%d",&a[i]);a[m+1]=n;
    A[0][1]=2,A[0][2]=1,A[1][2]=1;
    A[2][0]=1,A[2][1]=2,A[2][2]=2;
    A[0][0]=A[1][1]=1;
    B[0][1]=2,B[0][2]=1,B[1][2]=1;
    B[0][0]=B[1][1]=B[2][2]=1;
    R=ksm(A,a[1]);
    for(int i=1;i<=m;i++)R=R*B,R=R*ksm(A,a[i+1]-a[i]-1);
    printf("%d\n",R[0][2]);
    return 0;
} 

CXLIV.[IOI2018] meetings 会议

被人坑了说这题是CDQ分治的题,一小时想不出来开了题解发现是道DP

大概不会有人像我一样一开始想了极其诡异的DP,然后发现可以用莫队+树剖优化到 O(n\sqrt{n}\log^2n),但是这复杂度估计比 n^2 还差……

扯远了,下面是正解。

一个主流的想法是设 f[i,j] 表示 [i,j] 区间的答案,然后考虑转移。

考虑找到区间中的 \max 位置,不妨设为 k(如果有多个,随便取一个)。则,除非区间长为 1,否则在 k 这个位置集合一定不是最优解法,因为此时该区间中所有人的费用都是区间中最大值 a_k。集合位置要么是 k 左侧某个位置,然后右边的人走过来,代价全部为 k,要么相反。于是有

f[i,j]=\max\Big\{f[i,k-1]+(j-k+1)a_k,f[k+1,j]+(k-i+1)a_k\Big\}

(当然,对于 i>j 的状态,要么直接不予考虑,要么设其为 0

发现这两边形式一致,就可以只考虑一半东西,然后另一半直接将序列翻转再做一遍即可得到。但需要注意的是,两边转移的 k 值应该相同(这针对于有多个最大值的情形)。

我们不妨只考虑 f[k+1,j]+(k-i+1)a_k 这种转移。

因为转移多次涉及到区间 \max,所以有考虑笛卡尔树的可能。

然后发现,所有的 k 都是笛卡尔树上的一个节点,且 j 在其右侧子树中。

于是我们考虑求出从所有 k 开始,到其右侧子树中所有 jf[k+1,j] 值。

因为我们明显不能显式地求出所有 f[k+1,j](那样状态最劣仍是 O(n^2)),所以只能考虑借用旧的 f[k+1,j]。考虑用线段树维护。

然后我们发现,用线段树维护简直是一个天才般的想法,因为我们发现,[k+1,j] 这一段,刚好是 k 右儿子的区间。

这就意味着 k 的DP值可以在右儿子的结果上稍加修改就得到。但这同样意味着,为了格式统一,k 要上传给它的父亲 [i,j] 区间中,从 i 开始的所有值。

于是我们现在考虑求出如何求出 [i,j]

我们蓦然发现,实际上已经有了一半的值,因为 [i,k-1] 就是左儿子上传上来的DP值。而 [i,k] 又可以由 [i,k-1] 简单得到(准确地说,加上一个 a_k 就行了)。

于是现在考虑应该如何在 [k+1,j] 的基础上得到 [i,j]

祭出最上头的DP式,我们发现其有两种方式:一是左儿子中所有东西到右儿子去,右儿子中所有东西的DP值都要增加 (k-l+1)a_k,显然很好在线段树上区间加维护;二是右儿子中所有东西到左儿子去,这时候,位置 j 就要变成 f[i,k]+(j-k)a_k,是等差数列的形式!

但是我们悲哀地发现,要执行的是区间与等差数列取 \min。这显然不是线段树能办到的事。

但是,转机来了:因为 [k+1,j] 这段区间里所有元素都不大于 k,所以当从 f[k+1,j]f[k+1,j+1] 时,二者间 \Delta 值是一定 \leq a_k 的!而上面的那个一次函数,相邻两个位置间的差是恒为 a_k 的。这就意味着,如果从某个位置开始区间加的结果首次比等差数列小了,则其右方的东西一定也小,左方的东西一定都大。

因此,采取等差数列的部分一定是从 k+1 开始的连续区间。直接在线段树上二分就行了。

时间复杂度 O(n\log n)。(但是常数极大)

需要多说一句的是,这里的笛卡尔树没必要显式地搞出来,只需要求出每个节点所对应的区间就行了,直接用ST表即可。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[750010],st[750010][20],LG[750010],ql[750010],qr[750010];
bool sec;//if we are executing the second round 
ll res[750010];
int MAX(int x,int y){if(!sec)return a[x]>a[y]?x:y;else return a[x]>=a[y]?x:y;}
int RMQ(int x,int y){int k=LG[y-x+1];return MAX(st[x][k],st[y-(1<<k)+1][k]);}
#define lson x<<1
#define rson x<<1|1
#define mid ((l+r)>>1)
#define X x,l,r
#define LSON lson,l,mid
#define RSON rson,mid+1,r
struct SegTree{ll tagl,tagadd,lval,rval;int tagdelta;}seg[3001000];
void SETSEQ(int x,int l,int r,ll lval,int delta){seg[x].tagl=seg[x].lval=lval,seg[x].rval=lval+1ll*(r-l)*delta,seg[x].tagdelta=delta,seg[x].tagadd=0;}
void ADD(int x,ll val){if(seg[x].tagl!=-1)seg[x].tagl+=val;else seg[x].tagadd+=val;seg[x].lval+=val,seg[x].rval+=val;}
void pushup(int x){seg[x].lval=seg[lson].lval,seg[x].rval=seg[rson].rval;}
void pushdown(int x,int l,int r){
    if(seg[x].tagl!=-1){
        SETSEQ(LSON,seg[x].tagl,seg[x].tagdelta);
        SETSEQ(RSON,seg[x].tagl+1ll*seg[x].tagdelta*(mid-l+1),seg[x].tagdelta);
        seg[x].tagl=seg[x].tagdelta=-1;
    }else ADD(lson,seg[x].tagadd),ADD(rson,seg[x].tagadd),seg[x].tagadd=0;
}
void modifysequence(int x,int l,int r,int L,int R,ll leftval,int delta){
    if(l>R||r<L)return;
    if(L<=l&&r<=R){SETSEQ(X,leftval+1ll*(l-L)*delta,delta);return;}
    pushdown(X),modifysequence(LSON,L,R,leftval,delta),modifysequence(RSON,L,R,leftval,delta),pushup(x);
}
void modifyadd(int x,int l,int r,int L,int R,ll val){
    if(l>R||r<L)return;
    if(L<=l&&r<=R){ADD(x,val);return;}
    pushdown(X),modifyadd(LSON,L,R,val),modifyadd(RSON,L,R,val),pushup(x);
}
ll query(int x,int l,int r,int P){
    if(l==r)return seg[x].lval;
    pushdown(X);
    if(P<=mid)return query(LSON,P);else return query(RSON,P);
}
int innerbinaryonseg(int x,int l,int r,ll lval,int delta){
    if(l==r)return l;
    pushdown(X);
    if(seg[rson].lval<=lval+1ll*(mid+1-l)*delta)return innerbinaryonseg(LSON,lval,delta);
    else return innerbinaryonseg(RSON,lval+1ll*(mid+1-l)*delta,delta);
}
int outerbinaryonseg(int x,int l,int r,int L,int R,ll lval,int delta){
    if(l>R||r<L)return -1;
    if(L<=l&&r<=R){
//      printf("(%d,%d):%lld %lld\n",l,r,seg[x].lval,lval+1ll*(l-L)*delta);
//      printf("(%d,%d):%lld %lld\n",l,r,seg[x].rval,lval+1ll*(r-L)*delta);
        if(seg[x].rval>lval+1ll*(r-L)*delta)return -1;
        if(seg[x].lval<=lval+1ll*(l-L)*delta)return l-1;
        return innerbinaryonseg(X,lval+1ll*(l-L)*delta,delta);
    }
    pushdown(X);
    int tmp=outerbinaryonseg(LSON,L,R,lval,delta);if(tmp!=-1)return tmp;
    return outerbinaryonseg(RSON,L,R,lval,delta);
}
vector<int>v[750010];
void iterate(int x,int l,int r){
    if(l==r){printf("%lld ",seg[x].lval);return;}
    pushdown(X),iterate(LSON),iterate(RSON);
}
void solve(int l,int r){
    if(l==r){modifysequence(1,1,n,l,r,a[l],0);return;}
    int o=RMQ(l,r);
    if(l!=o)solve(l,o-1);
    if(r!=o)solve(o+1,r);
//  printf("[%d,%d]:%d\n",l,r,o);
    for(auto i:v[o])res[i]=min(res[i],query(1,1,n,qr[i])+1ll*(o-ql[i]+1)*a[o])/*,printf("QQ:%d\n",query(1,1,n,qr[i])+1ll*(o-ql[i]+1)*a[o])*/;
    ll FO;modifysequence(1,1,n,o,o,FO=(l==o?a[o]:query(1,1,n,o-1)+a[o]),0);
    if(r!=o)modifyadd(1,1,n,o+1,r,1ll*(o-l+1)*a[o]);
//  iterate(1,1,n),puts("");
    if(r!=o){
        int P=outerbinaryonseg(1,1,n,o+1,r,FO+a[o],a[o]);if(P==-1)P=r;//printf("%d\n",P);
        if(P>=o+1)modifysequence(1,1,n,o+1,P,FO+a[o],a[o]);
    }
//  iterate(1,1,n),puts("");
}
void calc(){
    for(int i=1;i<=n;i++)st[i][0]=i,v[i].clear();
    for(int j=1;j<=LG[n];j++)for(int i=1;i+(1<<j)-1<=n;i++)st[i][j]=MAX(st[i][j-1],st[i+(1<<(j-1))][j-1]);
    for(int i=1;i<=m;i++){int o=RMQ(ql[i],qr[i]);if(o!=qr[i])v[o].push_back(i);}
//  for(int i=1;i<=n;i++)printf("%d ",a[i]);puts("");
    solve(1,n);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=2;i<=n;i++)LG[i]=LG[i>>1]+1;
    for(int i=1,l,r;i<=m;i++){
        scanf("%d%d",&ql[i],&qr[i]),ql[i]++,qr[i]++;
        if(ql[i]!=qr[i])res[i]=0x3f3f3f3f3f3f3f3f;else res[i]=a[ql[i]];
    }
    calc();
    sec=true,reverse(a+1,a+n+1);for(int i=1;i<=m;i++)ql[i]=n-ql[i]+1,qr[i]=n-qr[i]+1,swap(ql[i],qr[i]);calc();
    for(int i=1;i<=m;i++)printf("%lld\n",res[i]);
    return 0;
}

CXLV.[九省联考2018]秘密袭击coat

首先先讲一种暴力但能过的方法。

很容易就会往每个值各被计算几次的方向去想。于是我们枚举每个节点,计算有多少种可能下该节点是目标节点。

为了避免相同的值的影响,我们在值相同的点间也决出一种顺序,即,若两个值相同的点在作比较,依照上文定下的那种顺序决定。

于是我们考虑从该枚举的点 x 出发,遍历整棵子树同时DP。设 f_{i,j} 表示 i 子树中有 j 个点的危险程度 \geq d_x。于是就直接背包转移就行了。

看上去复杂度是 O(n^3),但是加上下述两个优化就可以过了:

  1. 第二维最大只枚举到 m(这里的 m 即题面中的 k,因为 k 这个字母我们接下来还要用)

  2. 第二维最大只枚举到子树大小 sz

然后就过了,跑的还比正解都要快。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=64123;
int n,m,W,d[2010],f[2010][2010],p[2010],q[2010],res,sz[2010];
vector<int>v[2010];
void dfs(int x,int fa,int lim){
    for(int i=0;i<=sz[x];i++)f[x][i]=0;
    f[x][sz[x]=(q[x]>=lim)]=1;
    for(auto y:v[x]){
        if(y==fa)continue;
        dfs(y,x,lim);
//      printf("%d:",x);for(int i=0;i<=m;i++)printf("%d ",f[x][i]);puts("");
//      printf("%d:",y);for(int i=0;i<=m;i++)printf("%d ",f[y][i]);puts("");
        for(int i=sz[x];i>=0;i--)for(int j=min(m-i,sz[y]);j>=0;j--)(f[x][i+j]+=1ll*f[x][i]*f[y][j]%mod)%=mod;
        sz[x]=min(sz[x]+sz[y],m);
//      printf("%d:",x);for(int i=0;i<=m;i++)printf("%d ",f[x][i]);puts("\n");
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&W);
    for(int i=1;i<=n;i++)scanf("%d",&d[i]),p[i]=i;
    sort(p+1,p+n+1,[](int u,int v){return d[u]<d[v];});
    for(int i=1;i<=n;i++)q[p[i]]=i;
    for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    for(int i=1;i<=n;i++){
//      printf("%d\n",q[i]);
        if(q[i]>n-m+1)continue;
        dfs(i,0,q[i]);
        (res+=1ll*d[i]*f[i][m]%mod)%=mod;
    }
    printf("%d\n",res);
    return 0;
}

然后是正解。

我们要求 \sum\limits_{\mathbb S\subseteq\mathbb T}\text{Kth of }\mathbb S

考虑枚举该 \text{Kth} 的值为 i,则要求 \sum\limits_{i=1}i\sum\limits_{\mathbb S\subseteq\mathbb T}[\text{Kth of }\mathbb S=i]

考虑让每个 i 拆分成在所有的 j\leq i 的位置上各被计算一次,则要求 \sum\limits_{i=1}\sum\limits_{\mathbb S\subseteq\mathbb T}[\text{Kth of }\mathbb S\geq i]。(这同时也是期望DP的经典套路)

考虑令 cnt_i 表示 \geq i 的数的总数。则要求 \sum\limits_{i=1}\sum\limits_{\mathbb S\subseteq\mathbb T}[cnt_i\geq m]。(注意这里的 m 即为 k

考虑对于每个连通块,在树上最高处计算它的贡献。设 f_{i,j,k} 表示以 i 为根的子树内,当前统计的是 cnt_j,且 cnt_j=k 的方案数。转移是裸的背包卷积。

考虑如何求答案。因为我们是在最高处计算贡献,所以就要求 \sum\limits_{i=1}^n\sum\limits_{j=1}^W\sum\limits_{k=m}^nf_{i,j,k}

因为我们是卷积,所以考虑FFT转移。又因为它一直在卷,所以我们干脆考虑压根不把它复原成系数式,就纯粹用点集表示。

更准确地说,因为 f_{i,j} 的生成函数是 \sum\limits_{k=1}^nf_{i,j,k}x^k,一个 n 次多项式,所以我们直接枚举 x\in[1,n+1],然后分别求出这时的生成函数的值,最后拉格朗日插值一下就插回系数式了。

则,在合并父亲 x 和儿子 yf 数组时,因为是点集式,所以直接对应位置相乘就行了。

但是就算有了这么伟大的思路,我们算算复杂度,还是 O(n^3) 的。有没有更棒的复杂度?

我们发现,最终要对所有 xf 数组的和,倒不如在正常处理的过程中就顺便维护了。于是我们设 g_{i,j}=\sum\limits_{k\in\text{subtree}_i}f_{k,j},则最终要求的就是 \sum\limits_{i=m}^ng_{1,i}。当然,依据分配律,我们还是可以直接一股脑求出 \sum\limits_{i=1}^ng_{1,i},待插出系数式后再取 \geq m 的项。

我们思考当合并 f_{x,i}f_{y,i} 时会发生什么:

$g_{x,i}\rightarrow g_{x,i}+g_{y,i}

在全体结束后,再有 g_{x,i}\rightarrow g_{x,i}+f_{x,i}

同时,为了便于合并,我们采用线段树合并来维护DP数组。(这种操作被称作整体DP

我们考虑初始化,发现假如我们最外层枚举的点值是 X,则所有 \forall i\leq d_xf_{x,i} 在结束时都要乘上一个 X

明显这个初始状态大体是区间的,非常适合线段树合并。

但是,就算这样,暴力合并的复杂度仍然 O(n^3),必须考虑在区间上把它做掉。

于是,一个天才般的想法诞生了:

观察到每次的变换都是 (f,g)\rightarrow(af+b,cf+d+g) 的形式。

而这个变换,可以用四元组 (a,b,c,d) 唯一刻画。

同时,展开式子,就会发现这个变换具有结合律(虽然很遗憾的是,大部分情形下它不具有交换律)。

假如我们初始令 b=f,d=g 的话,就会发现,做一次上述操作,它就自动帮你更新了 fg

于是,我们把它看作区间的 tag,然后线段树合并就非常简单了。

同时,要注意的是,其单位元是 (1,0,0,0)

我们来总结一下操作:当合并的时候,我们希望 f_x\rightarrow f_x\times(f_y+1),而 f_y+1 可以通过在遍历完 y 的子树后打上全体 +1tag 解决,当然这里不需要额外增加其它的 tag,我们发现 (1,1,0,0) 刚好胜任了这个操作。于是现在 f_x\rightarrow f_{x}\times f_y(f_y,0,0,0)tag 即可(需要注意的是,f_y 是线段树上 y 处的 b)。g_{x}\rightarrow g_x+g_y(1,0,0,g_y) 即可,而 g_y 则是 d。两个乘起来,就是使用 (b,0,0,d)

最后合并 fg 的时候,则要使用 (1,0,1,0),意义通过展开即可得到就是将 f 加到 g。而乘上 X 的操作,使用 (X,0,0,0) 即可。

需要注意的是,这里并不能标记永久化,主要是因为从四元组中抽出 bd 的操作并非线性变换,不能打到 tag 上去,在线段树合并的时候要先一路下传到某一方已经没有叶子了再合并。

同时,使用 unsigned int 可以刚好把 64123 卡进一次乘法内不爆。

代码(一些比较疑惑的地方已经加了注释):

#include<bits/stdc++.h>
using namespace std;
#define int unsigned int
const int mod=64123;
int n,m,W,d[2010],X,cnt,bin[5010000],tp,rt[2010],a[2010];
struct dat{//(f,g)->(af+b,cf+d+g)
    int a,b,c,d;
    dat(){a=1,b=c=d=0;}
    dat(int A,int B,int C,int D){a=A,b=B,c=C,d=D;}
    friend dat operator*(const dat&u,const dat&v){return dat((u.a*v.a)%mod,(u.b*v.a+v.b)%mod,(u.a*v.c+u.c)%mod,(u.b*v.c+u.d+v.d)%mod);}
    void operator*=(const dat&v){(*this)=(*this)*v;}
    void print()const{printf("(%u %u %u %u)\n",a,b,c,d);}
};
#define mid ((l+r)>>1)
int newnode(){return tp?bin[tp--]:++cnt;}
struct SegTree{
    int lson,rson;
    dat tag;
}seg[5010000];
void erase(int &x){if(x)seg[x].tag=dat(),erase(seg[x].lson),erase(seg[x].rson),bin[++tp]=x,x=0;}//erase all the subtree of x.
void pushdown(int x){
    if(!seg[x].lson)seg[x].lson=newnode();
    if(!seg[x].rson)seg[x].rson=newnode();
    seg[seg[x].lson].tag*=seg[x].tag,seg[seg[x].rson].tag*=seg[x].tag,seg[x].tag=dat();
}
void modify(int &x,int l,int r,int L,int R,dat val){
    if(l>R||r<L)return;
    if(!x)x=newnode();
    if(L<=l&&r<=R){seg[x].tag*=val;return;}
    pushdown(x),modify(seg[x].lson,l,mid,L,R,val),modify(seg[x].rson,mid+1,r,L,R,val);
}
void merge(int &x,int &y){
    if(!seg[x].lson&&!seg[x].rson)swap(x,y);
    if(!seg[y].lson&&!seg[y].rson){seg[x].tag*=dat(seg[y].tag.b,0,0,seg[y].tag.d);return;}
    pushdown(x),pushdown(y),merge(seg[x].lson,seg[y].lson),merge(seg[x].rson,seg[y].rson);
}
int query(int x,int l,int r){
    if(l==r)return seg[x].tag.d;
    pushdown(x);
    return (query(seg[x].lson,l,mid)+query(seg[x].rson,mid+1,r))%mod;
}
void iterate(int x,int l,int r){
    if(!x)return;
    printf("%u:[%u,%u]\n",x,l,r);seg[x].tag.print();
    iterate(seg[x].lson,l,mid),iterate(seg[x].rson,mid+1,r);
}
vector<int>v[2010];
void dfs(int x,int fa){
    modify(rt[x],1,W,1,W,dat(0,1,0,0));//set all into (0,1,0,0),which means only f=1.
    for(auto y:v[x])if(y!=fa)dfs(y,x),merge(rt[x],rt[y]),erase(rt[y]);
    modify(rt[x],1,W,1,d[x],dat(X,0,0,0));//those <=d[x] are multiplied by an X
    modify(rt[x],1,W,1,W,dat(1,1,1,0));
    //product of (1,0,1,0) and (1,1,0,0), first means add f to g(to calculate the real g), second means add 1 to f (stands for x itself not chosen at x's father)
}
int all[2010],tmp[2010],res;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=x*x%mod)if(y&1)z=z*x%mod;return z;}
void Lagrange(){
    all[0]=1;
    for(int i=1;i<=n+1;i++)for(int j=i-1;j<=i;j--)(all[j+1]+=all[j])%=mod,(all[j]*=mod-i)%=mod;//note that j is unsigned!!!
//  for(int i=0;i<=n+1;i++)printf("%u ",all[i]);puts("");
    for(int i=1;i<=n+1;i++){
        int inv=ksm(mod-i),sum=0;
        for(int j=0;j<=n;j++)tmp[j]=all[j];
        for(int j=0;j<=n;j++)(tmp[j]*=inv)%=mod,(tmp[j+1]+=mod-tmp[j])%=mod;
//      if(i>=1410){for(int j=0;j<=n;j++)printf("%u ",tmp[j]);puts("");}
        for(int j=m;j<=n;j++)sum+=tmp[j];sum%=mod;
        for(int j=1;j<=n+1;j++)if(j!=i)(sum*=ksm((i-j+mod)%mod))%=mod;
        res+=sum*a[i]%mod;
    }
    res%=mod;
}
signed main(){
    scanf("%u%u%u",&n,&m,&W);
    for(int i=1;i<=n;i++)scanf("%u",&d[i]);
    for(int i=1,x,y;i<n;i++)scanf("%u%u",&x,&y),v[x].push_back(y),v[y].push_back(x);
    for(X=1;X<=n+1;X++)dfs(1,0),a[X]=query(rt[1],1,W),erase(rt[1]);
//  for(int i=1;i<=n+1;i++)printf("%u ",a[i]);puts("");
    Lagrange();printf("%u\n",res);
    return 0;
}

CXLVI.[十二省联考 2019]皮配

题解里”豌豆“的比喻实在太精妙了。

先重新描述一遍题意:有 n 个豆子,每个豆子有其重量,并位于某个豆荚内。每粒豆子颜色可以为黄色/绿色,表皮可以为皱皮/圆皮。每个豆荚里所有豆子的颜色必须相同。对于所有黄色/绿色/皱皮/圆皮的豆子,其重量和有一上界。有些豆子不能同时具有某两种性状,称其为”特殊的“。求总方案数。

首先,”重量和有上界“,想到背包问题。于是我们设计一种DP,f_{i,j} 表示黄色/皱皮的豆子分别的质量和,则绿色/圆皮的豆子可以用总质量减去得到。枚举每颗豆子是哪种具体性状,转移即可。

然后,我们发现,大部分时候,i,j 两维都是独立的——更准确地来说,对于那些不存在特殊豆子的豆荚来说,i 一维其就是在关于豆荚颜色背包;对于那些非特殊豆子来说,j 一维就是在关于豆子表皮背包。

于是我们设 f_i 表示黄色豆荚的总质量为 i 的方案数,g_i 表示皱皮豆子的总质量为 i 的方案数。显然复杂度皆为 O(nM)

因为 k 很小,所以特殊的豆子和豆荚数也很小,我们就把初始的状态搬过来,设 h_{i,j} 表示初始状态的意义,用它来处理特殊的东西即可。这部分时间复杂度 O\Big(k\times(ks)\times M\Big)=k^2sM,其中 k 是特殊豆子数,s 是豆子的最大质量。

最后就拼接 f,g,h 即可计算答案。(对于某个 h_{i,j},能与其构成合法方案的 fg 各是一段区间,故对其做前缀和即可简单维护)

需要注意存在空豆荚。

代码:

/*
Mendel's peas are Awesome! 
*/
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int T,n,m,q,yel,gre,rou,smo,id[1010],wei[1010],fob[1010],WEI[1010],f[2510],g[2510],h[2510][310],hh[2510][310],all,res;//yellow,green,rough,smooth
vector<int>v[1010];
bool FOB[1010];
int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m),memset(fob,-1,sizeof(fob));
        scanf("%d%d%d%d",&yel,&gre,&rou,&smo);
        for(int i=1;i<=n;i++)scanf("%d%d",&id[i],&wei[i]),WEI[id[i]]+=wei[i],v[id[i]].push_back(i),all+=wei[i];
        scanf("%d",&q);
        for(int i=1,x,y;i<=q;i++){
            scanf("%d%d",&x,&y);
            fob[x]=y,FOB[id[x]]=true;
        }
        f[0]=1;
        for(int i=1;i<=m;i++){
            if(FOB[i]||v[i].empty())continue;
            for(int j=yel-WEI[i];j>=0;j--)(f[j+WEI[i]]+=f[j])%=mod;
        }
        g[0]=1;
        for(int i=1;i<=n;i++){
            if(fob[i]!=-1)continue;
            for(int j=rou-wei[i];j>=0;j--)(g[j+wei[i]]+=g[j])%=mod;
        }
        for(int i=1;i<=yel;i++)(f[i]+=f[i-1])%=mod;
        for(int i=1;i<=rou;i++)(g[i]+=g[i-1])%=mod;
        h[0][0]=1;
        for(int i=1;i<=m;i++){
            if(!FOB[i]||v[i].empty())continue;
            memcpy(hh,h,sizeof(h));
            for(auto x:v[i]){
                if(fob[x]==-1)continue;
                if(fob[x]>1)for(int j=0;j<=yel;j++)for(int k=min(rou,300);k>=wei[x];k--)(hh[j][k]+=hh[j][k-wei[x]])%=mod;
                else if(fob[x]==1)for(int j=0;j<=yel;j++)for(int k=min(rou,300);k>=0;k--)hh[j][k]=(k>=wei[x]?hh[j][k-wei[x]]:0);

                if(fob[x]<=1)for(int j=0;j<=yel;j++)for(int k=min(rou,300);k>=wei[x];k--)(h[j][k]+=h[j][k-wei[x]])%=mod;
                else if(fob[x]==3)for(int j=0;j<=yel;j++)for(int k=min(rou,300);k>=0;k--)h[j][k]=(k>=wei[x]?h[j][k-wei[x]]:0);
            }
            for(int j=0;j<=yel;j++)for(int k=min(rou,300);k>=0;k--)if(j>=WEI[i])(h[j][k]+=hh[j-WEI[i]][k])%=mod;
        }
        gre=all-gre,smo=all-smo;
        for(int i=0;i<=yel;i++)for(int j=0;j<=min(rou,300);j++){
            int F=f[yel-i];
            if(i<gre)(F+=mod-f[gre-i-1])%=mod;
            int G=g[rou-j];
            if(j<smo)(G+=mod-g[smo-j-1])%=mod;
            (res+=1ll*h[i][j]*F%mod*G%mod)%=mod;
        }
        printf("%d\n",res);

        memset(f,0,sizeof(f)),memset(g,0,sizeof(g)),memset(h,0,sizeof(h));
        for(int i=1;i<=m;i++)WEI[i]=0,FOB[i]=false,v[i].clear();
        all=res=0;
    }
    return 0;
}

CXLVII.[NOI2016] 国王饮水记

首先,我们一定可以舍去那些高度比 h_1 还小的城市,并且将剩余的高度比 h_1 大的城市排序,使得 h_1h_n 递增。

我们不妨从三座城市想起。假如可以合并两次,应该怎么合并?

先合并 (1,2),再合并 (2,3),因为这样更多的水被贡献给了 1,对吧?

由此我们得出了第一个Obeservation:越早合并的城市,高度越低。这也就意味着,我们每次合并的城市,必定是高度递增的一段,且前一次合并的所有城市的水量都低于后一次合并的城市。

那如果只能合并一次呢?

首先,(1,3) 肯定是要合并的。那么,2 要不要并进去呢?这就要看 (1,3) 合并后,2 是否比 1,3 的高度高了。

于是我们得到了用一次合并来合并城市 1 与某段城市 [l,r] 的策略:先合并城市 1r,然后依次合并 r-1,r-2,\dots,l,直到并进去会使得平均值减小。

有了这两个观察,我们就可以开始DP了:设 f_{i,j} 表示前 i 个位置合并 j 次的最优答案。则 f_{i,j}=\max\limits_{k<i}\left\{\dfrac{f_{k,j-1}+s_i-s_k}{i-k+1}\right\}

同时,为了处理上面“合并 [l,r] 时实际上是合并了 [l,r] 中一段最优的后缀 [l',r]”的结论,f_{i,j} 还要与 f_{i-1,j},f_{i-2,j},\dots,f_{1,j}\max,用来表示位置 i 不合并到某段区间中。

这种“合并 k 次”的DP,要优化肯定要按照合并次数DP。于是我们设 f_i 表示当前的DP数组,g_i 表示上一轮DP(合并次数少一)的DP数组。

则式子修改为 f_i=\max\limits_{j<i}\left\{\dfrac{g_j+s_i-s_j}{i-j+1}\right\}

这时候,如果再观察到“合并次数 m 可以与 n-1\min”的话,就可以 O(n^3) 地直接用 double 暴力DP了。

可以取得 58 分的好成绩。代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,p,h[8010];
double f[8010],g[8010],s[8010];
void solve(){
    for(int i=1;i<=n;i++)f[i]=-0x3f3f3f3f3f3f3f3f;
    for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)f[i]=max(f[i],(g[j]+s[i]-s[j])/(i-j+1));
    for(int i=1;i<=n;i++)f[i]=max(f[i],f[i-1]);
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    for(int i=1;i<=n;i++)scanf("%d",&h[i]);
    sort(h+2,h+n+1),reverse(h+2,h+n+1);
    while(h[n]<=h[1])n--;
    reverse(h+2,h+n+1);
//  for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
    for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];
    m=min(m,n-1);
    for(int i=1;i<=n;i++)f[i]=h[1];
    while(m--){
        for(int i=1;i<=n;i++)g[i]=f[i];
        solve();
    }
    printf("%lf\n",f[n]);
    return 0;
}

然后,有一个推论是“合并 [l,r] 时实际上是合并了 [l,r] 中一段最优的后缀 [l',r]”这个东西实际上是没有必要的,因为若 [l,r] 只合并了 [l',r],而 [l,l'-1] 未被合并,但是以 l-1 结尾的一段区间却被合并了的话,我们完全可以将它向右移至以 l'-1 结尾,并使得答案更大。因此,这个结论变换为,所有被合并过的位置是 h 数组的一段后缀。(虽然这个结论是我在写题解时才发现的,因此代码中完全没有用到这个结论)

这之后,我们掏出转移式 f_i=\max\limits_{j<i}\left\{\dfrac{g_j+s_i-s_j}{i-j+1}\right\}

它可以被解释为 (s_i,i+1)(s_j-g_j,j) 两点间的斜率。

因为我们已经将 h 排过序,所以 s 就不止有递增的性质——它还是的!

尽管 (s_j-g_j,j) 可能在平面上随机分布,但是因为 s 的凹性,其就具有决策单调性(随着 i 的增大,每个点到它的斜率都在变大(这是由凹性决定的),而越大的 j 变大的速度就越快(因为所有的 j 的分子增长速度都是一致的,但是分母增长速度是 j 越大的越慢),终将在什么时候超越小的 j),因此我们可以对其进行斜率优化!

具体而言,发现最优决策点 j 只有可能在下凸壳上,于是就维护下凸壳,然后从下凸壳的队首进行转移即可。

明显单次复杂度就做到了 O(n)。这样子,若仍然用 double 暴力转移,复杂度是 O(n^2),可以取得 64 分的好成绩。

代码:

#include<bits/stdc++.h>
using namespace std;
typedef pair<double,double> pdd;
int n,m,p,h[8010],l,r;
double f[8010],g[8010],s[8010];
pdd dat[8010];
int q[8010];
double slope(pdd x,pdd y){return (x.second-y.second)/(x.first-y.first);}
void solve(){
    l=r=0;
    for(int i=1;i<=n;i++){
        dat[i]=make_pair(i,s[i]-g[i]);
        while(r-l>=2&&slope(dat[q[r-2]],dat[q[r-1]])>slope(dat[q[r-1]],dat[i]))--r;
        q[r++]=i;
        pdd I=make_pair(i+1,s[i]);
        while(r-l>=2&&slope(dat[q[l]],I)<slope(dat[q[l+1]],I))l++;
        f[i]=(g[q[l]]+s[i]-s[q[l]])/(i-q[l]+1);
    }
    for(int i=1;i<=n;i++)f[i]=max(f[i],f[i-1]);
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    for(int i=1;i<=n;i++)scanf("%d",&h[i]);
    sort(h+2,h+n+1),reverse(h+2,h+n+1);
    while(h[n]<=h[1])n--;
    reverse(h+2,h+n+1);
//  for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
    for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];
    m=min(m,n-1);
    for(int i=1;i<=n;i++)f[i]=h[1];
    while(m--){
        for(int i=1;i<=n;i++)g[i]=f[i];
        solve();
    }
    printf("%lf\n",f[n]);
    return 0;
}

到这里,优化似乎已经到头了——单次DP已经压到底线 O(n) 了。因此要优化只能从合并次数 m 这一维下手了。

因为水量互不相等,所以肯定是越晚合并的区间长度越短,不然我们一定可以将一个原本在后面合并的元素移到前面并使得答案增加。

依据这个结论,有个性质是若 k 较大时,决策的区间长度会很快收敛到 1。而又有一个结论是长度大于 1 的区间仅有最多 \log nh 个,约 14 个。

于是我们只需DP 14 层,剩下的部分全部选长度为 1 的区间就行了。

(证明?打表就行了!)

还有一个由“互不相同”带来的结论是,比较大小关系时精度只需要十几位就行了,这是显然的。于是我们DP时直接用普通 double 存,然后记录转移点,在DP结束后再用高精度小数按图索骥复原出完整的精度即可。

时间复杂度 O(n\log n+np)

代码(省略模板部分):

#include<bits/stdc++.h>
using namespace std;
typedef pair<double,double> pdd;
int n,m,p,h[8010],l,r,las[17][8010];
double f[8010],g[8010],s[8010];
pdd dat[8010];
int q[8010];
double slope(pdd x,pdd y){return (x.second-y.second)/(x.first-y.first);}
void solve(int ID){
    l=r=0;
    for(int i=1;i<=n;i++){
        dat[i]=make_pair(i,s[i]-g[i]);
        while(r-l>=2&&slope(dat[q[r-2]],dat[q[r-1]])>slope(dat[q[r-1]],dat[i]))--r;
        q[r++]=i;
        pdd I=make_pair(i+1,s[i]);
        while(r-l>=2&&slope(dat[q[l]],I)<slope(dat[q[l+1]],I))l++;
        f[i]=(g[q[l]]+s[i]-s[q[l]])/(i-q[l]+1),las[ID][i]=q[l];
    }
    for(int i=1;i<=n;i++)if(f[i]<f[i-1])f[i]=f[i-1],las[ID][i]=-1;
}
Decimal calc(int i,int j){
    if(!i)return h[1];
    if(las[i][j]==-1)return calc(i,j-1);
    return (calc(i-1,las[i][j])+s[j]-s[las[i][j]])/(j-las[i][j]+1);
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    for(int i=1;i<=n;i++)scanf("%d",&h[i]);
    sort(h+2,h+n+1),reverse(h+2,h+n+1);
    while(h[n]<=h[1])n--;
    reverse(h+2,h+n+1);
//  for(int i=1;i<=n;i++)printf("%d ",h[i]);puts("");
    for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];
    m=min(m,n-1);
    int lim=min(m,14);
    for(int i=1;i<=n;i++)f[i]=h[1];
    for(int j=1;j<=lim;j++){
        for(int i=1;i<=n;i++)g[i]=f[i];
        solve(j);
    }
    Decimal res=calc(lim,n-m+lim);
    for(int i=n-m+lim+1;i<=n;i++)res=(res+h[i])/2;
    cout<<res.to_string(p<<1)<<endl;
    return 0;
}

CXLVIII.[NOI2019] 机器人

首先发现每个点向左向右能到达的位置就类似笛卡尔树上一个点的代表区间,不同的是这里有多个最大值时选取最右的一个。于是我们可以想到一个DP,f_{[i,j],k} 表示区间 [i,j] 的最大值恰为 k 或不大于 k,两种设的方法均可。我们先以第一种方法为例。

我们有转移式 f_{[l,r],k}=\sum\limits_{i\text{ which satisfy the constraint}}\Big[k\in[a_i,b_i]\Big]\Big(\sum\limits_{k'\leq k}f_{[l,i-1],k'}\Big)\Big(\sum\limits_{k'<k}f_{[i+1,r],k'}\Big)。直接上即可拿到 35 分,而如果观察到因为单次转移合法的 i 数量是 O(1) 的,因而总会被访问到的状态数不会很多(准确地说, 2100 多一点),因此直接记忆化搜索处理即可拿到 50 分。

满分做法要猜一个结论:假如把所有 b_i 加一,得到的就都是左闭右开的区间 [a_i,b_i);然后就可以把整个大区间拆分成数量为 O(n) 级别的小区间 [c_1,c_2),[c_2,c_3),\dots,[c_{m-1},c_m),使得每个 [a_i,b_i) 都可以由几个小区间拼接得来。可以被证明的是,f_{[l,r]},对于每个 i,在 k\in[c_i,c_{i+1}) 时,均是一个不超过 r-l 次的多项式。

证明也很简单。首先,对于 l=r 的情形,这是显然成立的,对于 [c_i,c_{i+1})\subseteq[a,b) 的情形,所有的 f_{[l,r],k} 均为 1,是常数;对于 [c_i,c_{i+1})\nsubseteq[a,b) 的情形,均为 0,也是常数。然后尝试归纳。假设对于长度 \leq p 的区间皆成立,则考虑长度为 p+1 的区间 [l,r],其函数值是一堆 i 的函数和,因而只需证明对于上式中所有 i 得到的式子次数都不大于 r-l 即可。因为对于每个 i,后面的一大坨都是前缀和的形式,而众所周知的是,一个 n 次多项式的前缀和是 n+1 次多项式,也因此两个括号里的东西的次数分别为 i-l,r-i,乘起来时加一块就得到了 r-l 次多项式。

为了方便转移,我们需要维护前缀和(这里就与一开始设的不大于 k 的情形殊途同归了)。因为前缀和是 r-l+1 次多项式,所以我们只需要挑出 r-l+2,即最多 n+1 个点,插个值就能轻松求出前缀和。

我们显然可以取 c_i,c_i+1,c_i+2,\dots,c_i+nn+1 个点。这就意味着,我们只需DP出前 n+1 个点,剩下位置的前缀和就能够很快算出了。需要注意的是,c_i+n 可能大于 c_{i+1},这时不能插值,但是我们已经知道了每个位置的值,就直接暴力算前缀和即可。并且,因为下标连续,插值可以做到 O(n)

对于段 [c_i,c_{i+1})i 以前的段的值,也要被计入前缀和,但是因为对一切 k\in[c_i,c_{i+1}) 它都是常数,所以直接算到前缀和多项式的常数项中即可。

我们来计算复杂度。首先,我们要顺序DP每一段 [c_i,c_{i+1}),一共 O(n) 段;其次,对于每一段,我们都需要访问所有合法区间各一次,约共 2100 次;再次,对于每个合法区间,我们都需要 O(n) 地DP出前 n+1 个值,再 O(n) 地插值更新前缀和,加起来仍是 O(n);三项乘一块,得到总复杂度 O(2100n^2)

然后就是卡常了。注意到拉插时,对于同一个 [c_i,c_{i+1}),不同的位置计算的插值下标都相同,因而得到的拉格朗日基本多项式 l_i 也相同,影响的只有系数,因此可以在一起计算,这样使我本地的常数减少了 1s,放到网站上就是吸口氧过与不过的区别(反正NOI也吸氧)。

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int n,a[310],b[310],fac[310],inv[310],tmp[310],lim;
void ADD(int &x,int y){x+=y;if(x>mod)x-=mod;}
int g[2210][310],pre[2210][610],vis[3010],tot,id[310][310];
void Lagrange(int D,int L,int R){
    if(L+n>=R){for(int i=1;i<=tot;i++)pre[i][D]=g[i][lim];return;}
    tmp[0]=1;for(int i=1;i<=lim;i++)tmp[i]=1ll*tmp[i-1]*(R-(L+i-1))%mod;
    for(int j=1;j<=tot;j++)pre[j][D]=0;
    for(int i=lim,j=1;i;j=1ll*j*(R-(L+--i))%mod){
        int pmt=1ll*tmp[i-1]*j%mod*inv[i-1]%mod*inv[lim-i]%mod;
        if((lim-i)&1)for(int j=1;j<=tot;j++)ADD(pre[j][D],mod-1ll*pmt*g[j][i]%mod);
        else for(int j=1;j<=tot;j++)ADD(pre[j][D],1ll*pmt*g[j][i]%mod);
    }
}
vector<int>v;
void name(int l,int r){if(l>r||id[l][r])return;id[l][r]=++tot;if(l!=r)for(int i=(l+r-1)/2;i<=(l+r)/2+1;i++)name(l,i-1),name(i+1,r);}
int dfs(int l,int r,int D){
    if(l>r)return 0;
    int ID=id[l][r];
    if(vis[ID]==D)return ID;vis[ID]=D;
    if(l==r){
        if(D<a[l]||D>=b[l]){for(int i=0;i<=lim;i++)g[ID][i]=pre[ID][D-1];pre[ID][D]=pre[ID][D-1];}
        else{for(int i=0;i<=lim;i++)g[ID][i]=i+pre[ID][D-1];pre[ID][D]=v[D]-v[a[l]-1];}
        return ID;
    }
    for(int i=1;i<=lim;i++)g[ID][i]=0;g[ID][0]=pre[ID][D-1];
    for(int i=(l+r-1)/2;i<=(l+r)/2+1;i++){
        if(D<a[i]||D>=b[i])continue;
        int A=dfs(l,i-1,D),B=dfs(i+1,r,D);
        for(int j=1;j<=lim;j++)ADD(g[ID][j],1ll*g[A][j]*g[B][j-1]%mod);
    }
    for(int j=1;j<=lim;j++)ADD(g[ID][j],g[ID][j-1]);
    return ID;
}
int main(){
//  freopen("robot.in","r",stdin);
//  freopen("robot.out","w",stdout);
    scanf("%d",&n);
    fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
    inv[n]=ksm(fac[n]);for(int i=n-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod; 
    for(int i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]),b[i]++,v.push_back(a[i]),v.push_back(b[i]);
    sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
    for(int i=1;i<=n;i++)a[i]=lower_bound(v.begin(),v.end(),a[i])-v.begin()+1,b[i]=lower_bound(v.begin(),v.end(),b[i])-v.begin()+1;
//  for(auto i:v)printf("%d ",i);puts("");
//  for(int i=1;i<=n;i++)printf("[%d,%d)\n",a[i],b[i]);
    name(1,n);
    for(int i=0;i<=n+1;i++)g[0][i]=1;
    for(int i=1;i<v.size();i++){
//      printf("%d\n",i); 
        lim=min(n+1,v[i]-v[i-1]);
        for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)if(id[l][r])dfs(l,r,i);
        Lagrange(i,v[i-1],v[i]-1);
//      for(int l=1;l<=n;l++)for(int r=l;r<=n;r++)if(id[l][r]){printf("[%d,%d]",l,r);for(int j=0;j<lim;j++)printf("%d ",g[id[l][r]][j]);puts("");}
//      puts("");
    }
    printf("%d\n",pre[id[1][n]][v.size()-1]);
    return 0;
} 

CIL.[NOI2020] 制作菜品

本题有三个难点:留意到题面中的 n-2\leq m;证明;想到 bitset 优化。

首先,在很隐蔽的角落,有一句话 n-2\leq m\leq 5000。假如没看到这句话,就乖乖爆零罢。

结论1. m\geq n-1 时一定有解。

要证明这个结论,我们要分成三部分。

结论1.1. m=n-1 时一定有解。

不妨将 a 数组递增排序。则,a_1 必定 <k,因为所有数的平均值为 \dfrac{mk}{n}=\dfrac{(n-1)k}{n}<k,则必有至少一个数满足 <k 的条件。

同时,我们也能发现,a_1+a_n>k。因为,若 a_1+a_n\leq k,则 mk=\sum\limits_{i=1}^na_i\leq d_1+\sum\limits_{i=2}^na_n\leq a_1+(n-1)(k-a_1)=(n-1)k-(n-2)a_1=mk-(n-2)a_1。显然,当 n>2 时,因为 a_1>0,所以 (n-2)a_1>0,故可以一次填完所有 a_1,然后不足的部分就用 a_n 补,这样便同时少了一种原料和一道菜,转移到了 n-1 的情形;而当 n=2 时,因为只要做一道菜,所以直接把剩余的两种原料怼一块即可。

结论1.2. m=n 时一定有解。

不妨仍将 a 数组递增排序。则,a_n 必定 \geq k,因为所有数的平均值为 k,则必有至少一个数满足 \geq k 的条件。

a_n=k,则可以用 a_n 单独做一道菜,转移到 n,m 均减少 1 的情形,这样不断递归下去,直到 n,m 均为 0(此时已经构造出一组解)或是 a_n>k。当 a_n>k 时,仍可以用 a_n 单独做一道菜,转移到 m=n-1 的情形。

结论1.3. m>n 时一定有解。

递增排序后,a_n 必定 >k,因为所有数的平均值 >k,则必有至少一个数 >k。于是可以用 a_n 单独做一道菜,转移到 m 减一的情形。不断执行,直到到达 1.2 的场景。

结论2. m=n-2 时,当且仅当其能被分成两个非空集合 \mathbb{U,V} 使得 \sum\limits_{u\in\mathbb U}a_u=\Big(|\mathbb U|-1\Big)k 时,有解。

首先,其是充分的,因为对于 \mathbb{U,V} 我们可以分别应用 1.1 中的结论直接构造出一组解来。

其次,其是必要的,因为 n-2 条边连不出一张连通图,必定可以将所有原料分作两个集合使得不存在任何同时包含来自两个集合的原料的菜。

这样,我们就可以背包出一组解来了。具体而言,有经典套路是在上式中把 k 移到左侧,得到 \sum\limits_{u\in\mathbb U}(a_u-k)=-k。暴力背包复杂度是 O(n^3k) 的。但是,因为01背包的数组是 bool 数组,所以可以使用 bitset 优化至 \dfrac{n^3k}{\omega}

(可能因为 bitset 开太大的缘故,本代码在 Windows 下会 RE,可以使用 CF 的 Custom Test 编译)

代码:

#include<bits/stdc++.h>
using namespace std;
int T,n,m,p,a[510],ord[510];
vector<vector<int> >v;
bool cmp(int u,int v){return a[u]<a[v];}
bitset<5000010>bs[502];
const int half=2500002;
bool sd[510];
void SOLVE(int M,int N,int *dro){
    while(M){
        sort(dro+1,dro+N+1,cmp);
        v.push_back({dro[1],a[dro[1]],dro[N],p-a[dro[1]]});
        a[dro[N]]-=p-a[dro[1]];
        dro[1]=dro[N--],M--;
    }
}
int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%d%d%d",&n,&m,&p),v.clear();
        for(int i=1;i<=n;i++)scanf("%d",&a[i]),ord[i]=i;
        while(m>n){
            sort(ord+1,ord+n+1,cmp);
            v.push_back({ord[n],p}),a[ord[n]]-=p;
            m--;
        }
        while(n&&m==n){
            sort(ord+1,ord+n+1,cmp);
            v.push_back({ord[n],p}),a[ord[n]]-=p;
            m--;
            if(!a[ord[n]])n--;
        }
        if(m==n-1)SOLVE(m,n,ord);else if(n){
            bs[0].reset(),bs[0].set(half);
            for(int i=1;i<=n;i++){
//              printf("%d\n",i);
                if(a[i]==p)bs[i]=bs[i-1];
                if(a[i]>p)bs[i]=bs[i-1]|(bs[i-1]<<(a[i]-p));
                if(a[i]<p)bs[i]=bs[i-1]|(bs[i-1]>>(p-a[i]));
            }
            if(!bs[n].test(half-p)){puts("-1");continue;}
            for(int i=n,now=half-p;i;i--){
                if(bs[i-1].test(now-a[i]+p))sd[i]=true,now=now-a[i]+p;
                else sd[i]=false;
            }
//          for(int i=1;i<=n;i++)printf("%d ",sd[i]);puts("");
            sort(ord+1,ord+n+1,[](int x,int y){return sd[x]<sd[y];});
            for(int i=1;i<n;i++)if(!sd[ord[i]]&&sd[ord[i+1]])SOLVE(i-1,i,ord),SOLVE(n-i-1,n-i,ord+i);
        }
        for(auto i:v){for(auto j:i)printf("%d ",j);puts("");}
    }
    return 0;
} 

CL.[[NOI2018] 冒泡排序]()

结论1.交换次数压到下界,当且仅当不存在长度大于 2 的下降子序列。

证明很简单。众所周知的是,冒泡排序的交换次数等于序列逆序对数。要压到下界,与每个点有关的逆序对数都只能为 |i-p_i|,因为从 ip_i 的过程中本身就要交换 |i-p_i| 次。如果存在长度大于 2 的下降子序列的话,则逆序对就不仅是两两间贡献了,隔着中间一个数也能贡献,因而就压不到下界。

不存在长度大于 2 的下降子序列,等价于序列可以被划分为不超过 2 条上升子序列——我们抽出所有前缀 \max 构成一条上升子序列,则剩余的东西必然也递增,不然如果出现递减的两个数,从前面挑一个前缀 \max 就能构成长度为 3 的下降子序列。

要求出所有字典序 >q 的排列 p 个数,常见套路是枚举它们有长度为 i-1 的前缀相同,且第 ip 更大。

首先,我们可以判一下 q 的长度为 i-1 的前缀是否合法:首先,要确保其本身的非前缀 \max 元素递增;其次,要确保其未填入的元素全部 > 最后的非前缀 \max 元素。这样的话,就可以设计DP状态:f_{i,j} 表示有 i< 前缀 \max 的元素、j> 前缀 \max 的元素未填入时的方案数。则,f_{i,j}=f_{i-1,j}+\sum\limits_{k=1}^jf_{i+k-1,j-k},其中前半部分表示填入一个 <\max 的元素,显然为了保证递增只能填最小的一个;后半部分是枚举填入 >\max 的数中第 k 大的数填入,则前 k-1 个数都会被划归 <\max 的数。两个转移式可以被合并为 f_{i,j}=\sum\limits_{k=0}^jf_{i+k-1,j-k}。到这里 80 分已经到手了,但我们还可以考虑优化。

如果在矩阵上画出来的话,会发现这转移的位置是一条斜线,不好处理;但是我们可以转变DP状态:设新的 f_{i,j},其中 i 是原本的 i+j(总序列长度),j 是原本的 i< \max 的数的数量)。则有 f_{i,j}=\sum\limits_{k=0}^{i-j}f_{i-1,j+k-1}。考虑换成枚举 j+k-1,得到 f_{i,j}=\sum\limits_{k=\max(0,j-1)}^{i-1}f_{i-1,k}。到这里我们就发现之前贸然设的 k=0 的下界不合法,可能会出现负数,于是便重新设了 \max(0,j-1) 的下界。但是,更好的方式是,我们直接令 j<0f_{i,j}=0 即可直接使用 f_{i,j}=\sum\limits_{k=j-1}^{i-1}f_{i-1,k} 的转移式。

下面我们考虑用此DP值来求答案。枚举前缀长度 i,并设 j 表示后缀 [i+1,n] 中有多少个数 < \max。则此部分的贡献就是 \sum\limits_j f_{n-i,j}。现在考虑有哪些 j 是合法的。设若位置 i 老老实实填上了 q_i,剩余后缀中 <\max 的数量是 k

发现,就算 q_i 并非前缀 \max ,即可以填入 <\max 的数时,<\max 的数也不能填,因为所有未填的 <\max 的数中只能填最小的,而 q_i 一定不会比最小数还小,因此只能填入一个前缀最大值。故若 q_i 是前缀 \max ,贡献为 \sum\limits_{j=k+1}^{n-i}f_{n-i,j};否则,即 q_i 并非前缀 \max ,填入 i 位置的前缀 \max 会自动将 q_i 也归入非前缀 \max 类中,故 k 还是至少会加一。所以两部分综合起来看,都是 \sum\limits_{j=k+1}^{n-i}f_{n-i,j}

发现,无论是转移式还是求值式,我们都需要用到DP数组的后缀和。于是我们设 S_{n,m} 表示 \sum\limits_{i=m}^nf_{n,i}

好耶!是大家daisuki的推式子时间!

\begin{aligned}S_{n,m}&=\sum\limits_{i=m}^nf_{n,i}\\&=\sum\limits_{i=m}^n\sum\limits_{j=i-1}^{n-1}f_{n-1,j}\\&=\sum\limits_{j=m-1}^{n-1}\sum\limits_{i=m}^{j+1}f_{n-1,j}\\&=\sum\limits_{j=m-1}^{n-1}f_{n-1,j}+\sum\limits_{j=m-1}^{n-1}\sum\limits_{i=m+1}^{j-1}f_{n-1,j}\\&=S_{n-1,m-1}+\sum\limits_{i=m+1}^j\sum\limits_{j=i-1}^{n-1}f_{n-1,j}\\&=S_{n-1,m-1}+\sum\limits_{i=m+1}^jf_{n,i}\\&=S_{n-1,m-1}+S_{n,m+1}\end{aligned}

OK!这样我们就得出了简单的后缀和公式!

现在考虑其实际意义:从 S_{0,0}=1 出发,每次要么向 n,m 正方向各走一步,要么向 m 的负方向走一步。同时,因为在合法的转移式中可能会出现 S_{n,-1},但不会出现更负的数,因此我们便规定不能越过 m=-1 这条界线。

因为这个“不越过 -1 的界线”长得很像卡特兰数模型,因此我们考虑令 m 全体增加 1 再说。于是现在 S_{0,0}=S_{0,1}=1(原本有 S_{0,-1}=1,现在加一得到了 S_{0,0}=1)。两个起始状态觉得也不好做,因此我们再强制令全体 n 增加 1。于是现在 S_{1,0}=S_{1,1}=1。假如我们此时令 S_{0,0}=1 的话,就会发现由上述操作刚好可以得出 S_{1,0}=S_{1,1}=1,因此我们这个操作是正确的。

发现只有一种操作在 n 方向上有移动不好做。考虑让后一种操作也在 n 轴正方向上走一步。因为原本方案中 n 轴上动了恰好 n 步,故 m 轴正方向实际上也动了 n 步,则 m 轴负方向实际上动了 n-m 步;因为 m 轴负方向的移动现在同时在 n 轴正方向上移动,因此就多移动了 n-m 步,因此终点现在是 (2n-m,m)

发现现在是裸的卡特兰数模型。于是就直接得到 \dbinom{2n-m}{n-m}-\dbinom{2n-m}{n-m-1} 的卡特兰数。

因为我们之前强制令 n,m 各增加了 1,因此 S_{n,m} 中的 n,m 要比上式中的 n,m 各少一,所以此时要把它补回去,因此有 S_{n,m}=\dbinom{2n-m+1}{n-m}-\dbinom{2n-m+1}{n-m-1}

时间复杂度可以做到 O(n)。同时,在依次处理 q 的每个前缀的时候,要记得判断后缀中所有元素是否都 > 非前缀 \max 元素构成的序列中的 \max

代码:

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int N=1200000;
int T,n,q[1201000],fac[1201000],inv[1201000],premax,secmax,res,k,sufmin[1201000];
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int binom(int x,int y){if(x<y||y<0)return 0;return 1ll*fac[x]*inv[y]%mod*inv[x-y]%mod;}
int S(int x,int y){if(y>x)return 0;return(binom(2*x-y+1,x-y)-binom(2*x-y+1,x-y-1)+mod)%mod;}
int main(){
    fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1ll*fac[i-1]*i%mod;
    inv[N]=ksm(fac[N]);for(int i=N;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
    scanf("%d",&T);
    while(T--){
        scanf("%d",&n),premax=secmax=res=k=0;
        for(int i=1;i<=n;i++)scanf("%d",&q[i]);sufmin[n+1]=0x3f3f3f3f;
        for(int i=n;i;i--)sufmin[i]=min(sufmin[i+1],q[i]);
        for(int i=1;i<=n;i++){
            if(secmax>sufmin[i])break;
            if(q[i]>premax)k+=q[i]-premax-1,premax=q[i];else secmax=max(secmax,q[i]),k--;
            (res+=S(n-i,k+1))%=mod;
        }
        printf("%d\n",res);     
    }
    return 0;
}

到这里,又是50题结束了。敬请收看DP刷题笔记IV。