新高一暑假二期集训 Week 1

推荐博客园观看呢喵

8.11 DP优化杂题

[ROI2017] 前往大都会

写起来挺麻烦的一道题目。

首先因为第一问要求最短路,先跑一边 Dijkstra,又因为第二问依赖于最短路,所以再建出最短路图。

对于 \(u\) 连向 \(v\) 的边,如果在最短路图上,设 \(dis_i\) 表示 \(1\)\(i\) 最短路长,则 \(dis_u+w=dis_v\)

建出最短路图后,发现对于每条铁路,一定是一段一段出现在最短路图上的。

考虑设 \(dp_i\) 表示终点为 \(i\) 时的答案。

对于出现在最短路图上的一段铁路,设它的路径长前缀和数组为 \(sum\)

考虑铁路子段 \([l,r]\),可以转移 \(dp_r=\max(dp_r,dp_l+(sum_r-sum_l)^2)\)

考虑转移后效性,发现一定是 \(dis\) 小的向大的转移,所以按 \(dis\) 考虑从小到大枚举点并通过已有数据转移。

现在的复杂度为 \(O(m \log m + n^2)\),分别是最短路和转移部分的代价,考虑优化。

发现转移式是经典斜率优化的式子,所以可以用李超线段树优化。

具体地,将每条铁路上的每段连续铁路拆开成不同的铁路,然后记录每个点属于哪些铁路,并对于每段新铁路开李超线段树。

枚举到每个点时,就先在自己可能被更新的李超线段树取值更新,再对这些李超线段树去进行更新。

现在复杂度为 \(O(m \log m + n \log V)\),可以通过,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2000005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
bool vis[N];
queue<int> que;
vector<int> num[N],w[N];
vector<pair<int,int>> v[N],in[N];
int n,m,cnt,s[N],d[N],dp[N],rt[N];
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> q;
struct Lichao_segment_tree{
	struct line{int k,b;}l[N];
	int cnt=0,lcnt=0,tr[N<<5],ls[N<<5],rs[N<<5];
	void init(){
		fill(tr+1,tr+cnt+1,0),fill(ls+1,ls+cnt+1,0);
		fill(rs+1,rs+cnt+1,0),cnt=lcnt=0,l[0]={0,-INF};
	}
	int calc(int num,int x){return l[num].k*x+l[num].b;}
	void upd(int num,int l,int r,int &p){
		if(!p)  p=++cnt;
		int mid=(l+r)>>1;
		if(calc(num,mid)>calc(tr[p],mid))  swap(num,tr[p]);
		if(calc(num,l)>calc(tr[p],l))  upd(num,l,mid,ls[p]);
		if(calc(num,r)>calc(tr[p],r))  upd(num,mid+1,r,rs[p]);
	}
	void insert(int k,int b,int i){l[++lcnt]={k,b},upd(lcnt,0,1000000000,rt[i]);}
    int query(int x,int p,int l=0,int r=1000000000){
        if(x<l||r<x||!p)  return -INF;
        int mid=(l+r)>>1,sum=calc(tr[p],x);
        if(l==r)  return sum;
        return max({sum,query(x,ls[p],l,mid),query(x,rs[p],mid+1,r)});
    }
}LCT;
void dijkstra(int s){
    memset(d,0x3f,sizeof d),q.emplace(d[s]=0,s);
    while(!q.empty()){
        auto [sum,num]=q.top();q.pop();
        if(vis[num])  continue;
        vis[num]=1,que.push(num);
        for(auto [x,w]:v[num])
            if(d[x]>sum+w)
                q.emplace(d[x]=sum+w,x);
    }
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%lld",&s[i]);
        auto &num=::num[i],&w=::w[i];
        num.resize(s[i]+2,0),w.resize(s[i]+1,0);
        for(int j=1;j<=s[i]+1;j++){
            scanf("%lld",&num[j]);
            if(j!=s[i]+1)  scanf("%lld",&w[j]);
        }
        for(int j=1;j<=s[i];j++)
            v[num[j]].emplace_back(num[j+1],w[j]);
    }
    dijkstra(1),LCT.init();
    for(int i=1;i<=m;i++){
        int now=0,dis=0;
        auto &num=::num[i],&w=::w[i];
        for(int j=1;j<=s[i];j++){
            if(d[num[j]]+w[j]!=d[num[j+1]]){now=dis=0;continue;}
            if(!now)  now=++cnt,in[num[j]].emplace_back(cnt,0);
            dis+=w[j],in[num[j+1]].emplace_back(cnt,dis);
        }
    }
    while(!que.empty()){
        int x=que.front();que.pop();
        for(auto [num,dis]:in[x])
            dp[x]=max(dp[x],LCT.query(dis,rt[num])+dis*dis);
        for(auto [num,dis]:in[x])
            LCT.insert(-2*dis,dp[x]+dis*dis,num);
    }
    printf("%lld %lld\n",d[n],dp[n]);
}

[USACO16FEB] Circular Barn P

感觉很水的一道题,一眼就秒了,对于成环的题目,首先考虑破环为链,然后我们想怎么快速处理区间贡献。

\(sum1_i=\sum_{j=1}^i r_i\)\(sum2_i=\sum_{j=1}^i r_i\times i\),区间贡献 \(f_{l,r}=sum2_r-sum2_l-(sum1_r-sum1_l)\times l\)

然后发现 \(f_{l,r}\) 满足四边形不等式,证明:设 \(a<b<c<d\),则:

\[(f_{a,d}+f_{b,c})-(f_{a,c}+f_{a,d})= \]

\[(sum2_d-sum2_a-(sum1_d-sum1_a)\times a+(sum2_c-sum2_b-(sum1_c-sum1_b)\times b)- \]

\[(sum2_c-sum2_a-(sum1_c-sum1_a)\times a+(sum2_d-sum2_b-(sum1_d-sum1_b)\times b)= \]

\[(-sum1_d\times a-sum1_c\times b)-(-sum1_d\times b-sum1_c\times a)= \]

\[a\times (sum1_c-sum1_d)+b\times(sum1_d-sum1_c)=(sum1_d-sum1_c)(b-a)>0 \]

所以可以使用决策单调性优化,复杂度 \(O(nk\log n)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
int n,k,ans=INF,r[N],a[N],dp[10][N],sum1[N],sum2[N];
int calc(int l,int r){
	return sum2[r]-sum2[l]-(sum1[r]-sum1[l])*l;
}
void solve(int now,int l,int r,int pl,int pr){
	if(l>r)	return;
	int mid=(l+r)>>1,pos;dp[now][mid]=INF;
	for(int i=pl;i<=min(pr,mid);i++){
		int tmp=dp[now-1][i-1]+calc(i,mid);
		if(tmp<dp[now][mid])  dp[now][mid]=tmp,pos=i;
	}
	solve(now,l,mid-1,pl,pos),solve(now,mid+1,r,pos,pr);
}
void work(){
    for(int i=1;i<=n;i++)  sum1[i]=sum1[i-1]+a[i];
    for(int i=1;i<=n;i++)  sum2[i]=sum2[i-1]+a[i]*i;
    for(int i=1;i<=k;i++)  solve(i,1,n,1,n);
    ans=min(ans,dp[k][n]);
}
signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<=n;i++)  scanf("%lld",r+i);
    for(int i=1;i<=n;i++){
        int cnt=0;
        for(int j=i;j<=n;j++)  a[++cnt]=r[j];
        for(int j=1;j<i;j++)   a[++cnt]=r[j];
        fill(dp[0]+1,dp[0]+n+1,INF),work();
    }
    printf("%lld\n",ans);
}

[Becoder51713] 仙人掌

首先发现固定 \(l\) 之后,\(r\) 越大满足 \(\sum a_i=w\) 的最小满足条件的 \(\sum b_i\) 就越小。

所以可以考虑双指针,对于每个 \(l\) 找到满足题意的最小的 \(r\),并统计贡献。

但有个问题就是移动 \(l\) 的时候背包不好退贡献,所以要用到一个双栈的 trick。

具体来讲我们可以通过两个维护背包数组的栈 \(S\)\(T\) 来进行区间的增删操作。

其中每个元素都是维护从当前元素一直到栈底的答案。

对于右端点的加入操作,直接将 \(S\) 栈顶的背包数组提取出来进行操作后加进 \(S\)

而删除操作则依赖于 \(T\) 栈进行,具体地:

\(T\) 栈为空,则将 \(S\) 栈里面的元素依次加入到 \(T\) 中,这样 \(T\) 维护的就是反顺序的信息,即从上到下的下标递增。

然后直接将 \(T\) 的栈顶给 pop 出去,这样下标最小的就直接没了。

每次查询就直接将两个栈的栈顶在 \(w\) 的位置单点 merge 一下,这道题就这样愉快地完成了,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 10005
#define INF 0x3f3f3f3f
int n,m,k,ans=INF,a[N],b[N];
stack<pair<int,vector<int>>> S,T;
bool check(){
    vector<int> a=S.top().second;
    vector<int> b=T.top().second;int res=INF;
    for(int i=0;i<=m;i++)  res=min(res,a[i]+b[m-i]);
    return res<=k;
}
void insert(vector<int> &a,int v,int w){
    for(int i=m;i>=v;i--)  a[i]=min(a[i],a[i-v]+w);
}
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=n;i++)  scanf("%d%d",a+i,b+i);
    vector<int> st(m+1,INF);
    st[0]=0,S.emplace(0,st),T.emplace(0,st);
    for(int l=1,r=0;l<=n;l++,T.pop()){
        while(r<n&&!check()){
            vector<int> tmp=S.top().second;
            r++,insert(tmp,a[r],b[r]),S.emplace(r,tmp);
        }
        if(check())  ans=min(ans,r-l+1);
        if(!T.top().first){
            while(S.top().first){
                int pos=S.top().first;S.pop();
                vector<int> tmp=T.top().second;
                insert(tmp,a[pos],b[pos]),T.emplace(pos,tmp);
            }
        }
    }
    printf("%d\n",ans<=n?ans:-1);
}

8.12 DP杂题

[HDU6566] The Hanged Man

今天最难写的一道题,首先有一个显而易见的 \(O(nm^2)\) 树上背包做法。

然后我们需要改变转移的顺序,对原树进行树链剖分,然后先遍历轻儿子再遍历重儿子得到 dfn 序。

然后我们按 dfn 序每次由 \(i\)\(i+1\) 转移,可以发现 \(i+1\) 要么是 \(i\) 的儿子要么是 \(i\) 所在重链的父亲。

所以我们可以对于每个 \(i\) 状压 \(1\)\(i\) 途经所有重链的链底来维护信息,因为 \(1\)\(i\) 路径左边的节点过后就是用不到的。

然后因为每个点到 \(1\) 的路径最多经过 \(O(\log n)\) 个重链,所以每个点的状态数是 \(O(2^{\log n})=O(n)\) 的。

总的复杂度就从 \(O(nm^2)\) 变成 \(O(n^2m)\) 了,可以通过,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 55
#define M 5005
#define int long long
vector<int> v[N],line[N];
int t,n,m,cs,cnt,a[N],b[N],sz[N],dy[N];
int dep[N],dfn[N],pre[N],son[N],top[N];
struct Node{
    int val,sum;
    Node(int _val=0,int _sum=0){val=_val,sum=_sum;}
    void operator+=(const Node& x){
        if(val==x.val)  sum+=x.sum;
        else if(val<x.val)  val=x.val,sum=x.sum;
    }
}dp[2][N][M];
void dfs1(int x){
    dep[x]=dep[pre[x]]+1,sz[x]=1,son[x]=0;
    for(auto y:v[x]){
        if(y==pre[x])  continue;
        pre[y]=x,dfs1(y),sz[x]+=sz[y];
        if(sz[y]>sz[son[x]])  son[x]=y;
    }
}
void dfs2(int x){
    dfn[x]=++cnt,dy[cnt]=x;
    for(auto y:v[x]){
        if(y==pre[x]||y==son[x])  continue;
        top[y]=y,dfs2(y);
    }
    if(son[x])  top[son[x]]=top[x],dfs2(son[x]);
}
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld%lld",&n,&m),cnt=0;
        for(int i=1;i<=n;i++)  scanf("%lld%lld",a+i,b+i);
        for(int i=1,x,y;i<n;i++){
            scanf("%lld%lld",&x,&y);
            v[x].push_back(y),v[y].push_back(x);
        }
        int nxt=1,now=0;
        dfs1(1),top[1]=1,dfs2(1),dp[now][0][0]={0,1},dp[now][1][a[dy[1]]]={b[dy[1]],1};
        for(int i=1;i<=n;i++){
            int tmp=i;
            while(tmp){line[i].push_back(tmp),tmp=pre[top[tmp]];}
        }
        for(int i=2;i<=n;i++,swap(now,nxt)){
            int x=dy[i],ns=1<<line[x].size();
            for(int j=0;j<N;j++)  fill(dp[nxt][j],dp[nxt][j]+M,Node(0,0));
            int pre=dy[i-1],fa=-1,ps=1<<line[pre].size();
            vector<int> mp(line[pre].size(),-1);
            for(int j=0;j<line[pre].size();j++){
                if(line[pre][j]==::pre[x])  fa=j;
                for(int k=0;k<line[x].size();k++)
                    if(line[pre][j]==line[x][k]){mp[j]=k;break;}
            }
            for(int s=0;s<ps;s++){
                int nxts=0;
                for(int j=0;j<line[pre].size();j++)
                    if((s>>j&1)&&mp[j]!=-1)  nxts|=(1<<mp[j]);
                for(int j=0;j<=m;j++){
                    if(!dp[now][s][j].sum)  continue;
                    dp[nxt][nxts][j]+=dp[now][s][j];
                    if(!(s>>fa&1)&&j+a[x]<=m)
                        dp[nxt][nxts|1][j+a[x]]+=Node(dp[now][s][j].val+b[x],dp[now][s][j].sum);
                }
            }
        }
        printf("Case %lld:\n",++cs);
        for(int i=1;i<=m;i++){
            Node ans(0,0);
            for(int s=0;s<(1<<line[dy[n]].size());s++)  ans+=dp[now][s][i];
            printf("%lld%s",ans.sum,i==m?"\n":" ");
        }
        for(int i=1;i<=n;i++)  v[i].clear(),line[i].clear();
        for(int i=0;i<2;i++)  for(int j=0;j<N;j++)  fill(dp[i][j],dp[i][j]+M,Node(0,0));
    }
}

[Wannafly挑战赛24] 旅行

一眼离线点分治,先考虑如何从父亲处转移到当前节点值。

\(f_{i,j}\) 表示根到 \(i\) 选取的城市(不包含根)美丽值之和模 \(k\) 的值为 \(j\) 时的方案数,则 \(f_{i,j}\leftarrow f_{fa_i,j}\)\(f_{i,(j+a_i)\%k}\leftarrow f_{fa_i,j}\)

然后考虑合并两条路径,首先将根加入一条路径,设加入过后两个数组为 \(f\)\(g\),则 \(ans=f_0\times g_0 + \sum_{i=1}^k f_i \times g_{k-i}\)

最后要特判两个点相同的情况,设这个点为 \(x\),则 \(ans=[a_x=0]+1\)

因为是离线,所以要考虑将询问挂到什么地方,显然不能以每个点为根时都扫一遍能不能统计答案。

所以考虑先把所有询问挂到全树的重心上,每个根提取询问的时候,若在不同子树内就统计答案,否则就挂到对应子树上。

这样因为点分治只有 \(O(\log n)\) 层,所以每个询问最多就只会被挂 \(O(\log n)\) 次,复杂度得到保证。

最终的复杂度即为 \(O(nk \log n+q\log n)\),可以通过,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define int long long
bool del[N];
vector<int> v[N];
const int mod=998244353;
vector<tuple<int,int,int>> que[N],tmp[N];
int n,k,q,sum,smax,root;
int a[N],in[N],sz[N],now[N],ans[N],dp[N][55];
void getroot(int u,int fa){
	sz[u]=1;int dmax=0;
	for(auto v:v[u]){
		if(v==fa||del[v])  continue;
		getroot(v,u),sz[u]+=sz[v],dmax=max(dmax,sz[v]);
	}
	dmax=max(dmax,sum-sz[u]);
	if(dmax<smax)  smax=dmax,root=u;
}
void dfs(int u,int fa){
    copy(dp[fa],dp[fa]+k,dp[u]);
    for(int i=0;i<k;i++){
        int nxt=(i+a[u])%k;
        dp[u][nxt]=(dp[u][nxt]+dp[fa][i])%mod;
    }
    for(auto v:v[u]){
        if(v==fa||del[v])  continue;
        in[v]=in[u],dfs(v,u);
    }
}
void solve(int u){
	del[u]=1,fill(dp[u],dp[u]+k,0),dp[u][0]=1,in[u]=u;
    for(auto v:v[u])  if(!del[v])  dfs(in[v]=v,u);
    for(auto [x,y,i]:que[u]){
        if(in[x]!=in[y]){
            copy(dp[x],dp[x]+k,now);
            for(int j=0;j<k;j++){
                int nxt=(j+a[u])%k;
                now[nxt]=(now[nxt]+dp[x][j])%mod;
            }
            ans[i]=now[0]*dp[y][0]%mod;
            for(int j=1;j<k;j++)
                ans[i]=(ans[i]+now[j]*dp[y][k-j])%mod;
        }
        else  tmp[in[x]].emplace_back(x,y,i);
    }
	for(auto v:v[u]){
		if(del[v])  continue;
		sum=smax=sz[v],getroot(v,u);
        que[root]=tmp[v],tmp[v].clear(),solve(root);
	}
}
signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1,x,y;i<n;i++){
        scanf("%lld%lld",&x,&y);
        v[x].push_back(y),v[y].push_back(x);
    }
    for(int i=1;i<=n;i++)  scanf("%lld",a+i);
    scanf("%lld",&q),sum=smax=n,getroot(1,smax);
    for(int i=1,x,y;i<=q;i++){
        scanf("%lld%lld",&x,&y);
        if(x==y){ans[i]=(!a[x])+1;continue;}
        que[root].emplace_back(x,y,i);
    }
    solve(root);
    for(int i=1;i<=q;i++)  printf("%lld\n",ans[i]);
}

[MdOI R2] Resurrection

思维很神奇的一道题目,不难发现生成的图 \(G\) 也是一棵树。

在两张图均以 \(n\) 为根的情况下,\(G\) 中所有点的父亲都是 \(T\) 中对应点的祖先。

而且将 \(G\) 中的所有边连在 \(T\) 上时不会产生交叉,考虑证明这个结论:

已知一条边如果要在 \(G\) 中出现,那在 \(T\) 中断开构造产生它们的边时,这条边端点在 \(T\) 上的路径一定不能断开。

这是显然的,因为如果断开了那 \(G\) 中的这条边就无法被构造出来。

如果两条边 \(a\)\(b\)\(c\)\(d\) 交叉了:

1.如果先断的边是交叉部分,那后断的边就不符合路径不断开的条件了,在 \(G\) 中就连不上了。

2.如果不是交叉部分,假设先断 \(a\)\(b\),那在 \(c\)\(d\) 的那一段的最大值就应该是 \(a\)\(b\)\(c\)\(d\)\(G\) 中就也连不上边。

通过反证我们确定了 \(G\) 中的所有边连在 \(T\) 上时不会产生交叉是 \(G\) 满足条件的必要条件。

那是否充分呢,考虑这样构造方案:维护一个集合,一开始只有根的子节点。

每次取集合中编号最小的点,断开它在原树中到父亲的边,然后将它在新树中的子节点加入集合。

这样构造的方案一定是合法的:

考虑点 \(u\) 到父亲的连边,这个点不可能比它的目标父亲 \(v\) 大,因为只有在 \(v\) 到父节点的边被断开后 \(u\) 才可能加入集合。

这个点也不可能比 \(v\) 小:

如果比 \(v\) 小,那么 \(u\)\(v\) 中一定有边被断开了,但这些点的编号都比 \(u\) 大,它们必须比 \(u\) 先加入集合才能出现这种情况。

然而,根据这个条件的必要性,\(v\) 一定是这些节点在 \(G\) 中的祖先节点,所以这些节点都不早于 \(u\) 加入集合,矛盾。

所以这个点既不可能比 \(v\) 大也不能比 \(v\) 小,那就只能是 \(v\) 了。

我们就确定了 \(G\) 中的所有边连在 \(T\) 上时不会产生交叉是 \(G\) 满足条件的充要条件。

考虑直接计算这样的图的方案数,设 \(f_{i,j}\) 表示考虑到节点 \(i\),祖先中有 \(j\) 个可以向其连边的节点时,子树中连边的方案数。

考虑让点 \(u\) 和从下往上第 \(x\) 个节点连边。那么这条连线会覆盖掉 \(x−1\) 个可以连边的节点。

接着对于子树中的所有节点,就都可以和 \(u\) 进行连边了。

得出转移式为:\(f_{i,j}=\sum_{x=1}^j\prod f_{v,j-(x-1)+1}\),前缀和优化复杂度为 \(O(n^2)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 3005
#define int long long
vector<int> v[N];
const int mod=998244353;
int n,dp[N][N],sum[N][N];
int cmp(int x,int y){return (x+y)%mod;}
void dfs(int x,int fa){
    fill(dp[x],dp[x]+n+1,1);
    for(auto y:v[x]){
        if(y==fa)  continue;
        dfs(y,x);
        for(int i=0;i<=n;i++)
            dp[x][i]=dp[x][i]*sum[y][i+1]%mod;
    }
    partial_sum(dp[x]+1,dp[x]+n+1,sum[x]+1,cmp);
}
signed main(){
    scanf("%lld",&n);
    for(int i=1,x,y;i<n;i++){
        scanf("%lld%lld",&x,&y);
        v[x].push_back(y),v[y].push_back(x);
    }
    dfs(n,0),printf("%lld\n",dp[n][0]);
}

[JOI2012] kangaroo

先将袋鼠体积从大到小排序,设 \(t_i\) 表示第 \(i\) 只袋鼠能被装进 \(t_i\) 个口袋里。

考虑定义状态 \(f_{i,j,k}\) 表示考虑到第 \(i\) 只袋鼠,\(t_i\) 个口袋里装了 \([1,i]\)\(j\) 只袋鼠和 \([i+1,n]\)\(k\) 只袋鼠。

所以 \(t_i\) 个口袋被分为了三类:

1.装 \([1,i]\) 中的袋鼠的 \(j\) 个。

2.装 \([i+1,n]\) 中的袋鼠的 \(k\) 个。

3.没装袋鼠的自由节点共 \(t_i-j-k\) 个。

考虑向后转移 \(f_{i,j,k}\),对于第 \(i+1\) 只袋鼠共有三种情况:

1.不被装进别的口袋,则 \(t_i\) 个口袋必须用完,而 \([1,i]\) 用了 \(j\) 个,那 \([i+2,n]\) 就要用 \(t_i-j\) 个,所以 \(f_{i+1,j,t_i-j} \leftarrow f_{i,j,k}\)

2.被装进自由节点中,因为有 \(t_i-j-k\) 个,所以 \(f_{i+1,j+1,k} \leftarrow f_{i,j,k}\times(t_i-j-k)\)

3.对于 \(i\),被装进了第二类口袋中,因为有 \(k\) 个,所以 \(f_{i+1,j+1,k-1} \leftarrow f_{i,j,k}\times k\)

统计答案 \(ans=\sum_{j=0}^nf_{n,j,0}\),时间复杂度 \(O(n^3)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 305
#define int long long
const int mod=1000000007;
void add(int &x,int y){x=(x+y)%mod;}
int n,now=1,pre=0,ans,a[N],b[N],t[N],dp[2][N][N];
signed main(){
    scanf("%lld",&n),dp[pre][0][0]=1;
    for(int i=1;i<=n;i++)  scanf("%lld%lld",a+i,b+i);
    sort(a+1,a+n+1,[](int x,int y){return x>y;});
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)  t[i]+=(a[i]<b[j]);
    for(int i=0;i<n;i++,swap(pre,now)){
        for(int j=0;j<N;j++)  fill(dp[now][j],dp[now][j]+N,0);
        for(int j=0;j<=i;j++){
            for(int k=0;k+j<=t[i+1];k++){
                add(dp[now][j][t[i+1]-j],dp[pre][j][k]);
                add(dp[now][j+1][k],dp[pre][j][k]*(t[i+1]-j-k));
                if(k)  add(dp[now][j+1][k-1],dp[pre][j][k]*k);
            }
        }
    }
    for(int i=0;i<=n;i++)  add(ans,dp[pre][i][0]);
    printf("%lld\n",ans);
}

8.13 DP 杂题

[JOI Open 2016] 摩天大楼

考虑连续段 DP,设 \(f_{i,j,k}\) 表示考虑到第 \(i\) 个数,有 \(j\) 个连续段,这些数产生的贡献为 \(k\) 的方案数。

然后会有一个连续段 DP 的经典转移,但是假设前 \(\frac{n}{2}\) 个数如果在最终序列中恰好全都不相邻。

那在只处理这些数时,贡献就是 \(-n\times L\),总复杂度就是 \(O(n^3L)\),过不了。

所以考虑怎么将第三维降低到 \(O(L)\) 的级别,假设我们从小到大对 \(a\) 数组进行排序。

对于 \(i>j\),将 \(a_i-a_j\) 拆成 \((a_i-a_{i-1})+(a_{i-1}-a_{i-2})+\dots +(a_{j+1}-a_j)\) 这样的差分形式。

这样我们每次考虑 \(i\rightarrow i+1\) 的时候,就只需要考虑 \(a_{i+1}-a_i\) 的贡献,而这个贡献一定是正的,第三维复杂度就降下来了。

接着我们考虑 \(a_{i+1}-a_i\) 会产生多少次贡献,以下标为 \(x\) 轴,\(a_i\)\(y\) 轴画出所有的连续段。

发现每一个连续段都呈现一个对勾的形式,而对于每一个连续段的两端,除了已确定的头和尾,都会向上增长。

而只要向上增长,\(a_{i+1}-a_i\) 的贡献就会被包含在内,那就会产生 \(j\times-p-q\) 次贡献。

其中,\(j\) 表示连续段数量,\(p\)\(q\) 分别表示头尾是否被确定。

那我们修改状态为 \(dp_{i,{0/1},{0/1},j,k}\) 表示考虑到第 \(i\) 个数,头尾是否被确定,有 \(j\) 个连续段,贡献为 \(k\) 的方案数。

接着进行连续段 DP,按刚才所述处理贡献,就可以在 \(O(n^2L)\) 的复杂度完成问题了,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 105
#define M 1005
#define int long long
const int mod=1000000007;
int n,m,ans,a[N],dp[2][2][2][N][M];
void add(int &x,int y){x=(x+y)%mod;}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)  scanf("%lld",a+i);
	if(n==1)  return printf("1\n"),0;
	if(n==2)  return printf("%lld\n",(abs(a[1]-a[2])<=m)*2),0;
	sort(a+1,a+n+1);
	for(int p=0;p<2;p++)  for(int q=0;q<2;q++)  dp[0][p][q][1][0]=1;
	dp[0][1][1][1][0]=0;
	for(int i=2;i<=n;i++){
		memset(dp[1],0,sizeof dp[1]);
		for(int j=0;j<i;j++)  for(int k=0;k<=m;k++){
			for(int p=0;p<2;p++)  for(int q=0;q<2;q++)  if(dp[0][p][q][j][k]){
				int to=k+(2*j-p-q)*(a[i]-a[i-1]);
				if(to>m)  continue;
				add(dp[1][p][q][j+1][to],dp[0][p][q][j][k]*((j+1)-p-q));
				if(!p)  add(dp[1][1][q][j+1][to],dp[0][p][q][j][k]);
				if(!q)  add(dp[1][p][1][j+1][to],dp[0][p][q][j][k]);
				add(dp[1][p][q][j][to],dp[0][p][q][j][k]*((j<<1)-p-q));
				if(!p)  add(dp[1][1][q][j][to],dp[0][p][q][j][k]);
				if(!q)  add(dp[1][p][1][j][to],dp[0][p][q][j][k]);
				if(j)  add(dp[1][p][q][j-1][to],dp[0][p][q][j][k]*(j-1));
			}
		}
		swap(dp[0],dp[1]);
	}
	for(int i=0;i<=m;i++)  add(ans,dp[0][1][1][1][i]);
	printf("%lld\n",ans);
}

[ABC266Ex] Snuke Panic(2D)

定义 \(f_i\) 表示路径以第 \(i\) 个洞为路径结尾时的答案。

发现 \(i\) 能向 \(j\) 转移的前提是 \(t_i<t_j\)\(y_i<y_j\)\((t_j-t_i)-(y_j-y_i+\lvert x_j-x_i\rvert)\geq 0\)

能看出这个转移条件大概是个三维偏序的关系,考虑使用 cdq 分治。

对于最外层的排序,以 \(y_i\) 为第一关键字,\(t_i\) 为第二关键字从大到小进行排序。

因为当 \(y\)\(t\) 相等时,由于不会有完全相同的两个洞,所以出现这种情况时是无法转移的,那么就不需要第三关键字。

而在内部,因为绝对值贡献在数据结构中不好处理贡献,考虑将绝对值拆开,将 \(x\) 从小到大和从大到小各转移一次。

如果让 \(x\) 从小到大转移,那么 \(i\)\(j\) 转移条件就变为\((t_j-t_i)-(y_j-y_i+x_j-x_i)\geq 0\)

整理一下变成 \(t_j-x_j-y_j \geq t_i-x_i-y_i\),就可以用树状数组离散化后跑前缀 max,那这个题就做完了。

复杂度 \(O(n \log^2 n)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 100005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
struct Node{int t,x,y,a,i;}s[N];
int n,ans,cnt,wz[N],dp[N],tr[N],rk[N],tmp[N];
bool cmp1(Node a,Node b){
	if(a.y!=b.y)  return a.y<b.y;
	return a.t<b.t;
}
bool cmp2(Node a,Node b){return a.x<b.x;}
void update(int x,int y){while(x<=cnt)  tr[x]=max(tr[x],y),x+=x&-x;}
int query(int x,int res=-INF){while(x)  res=max(res,tr[x]),x-=x&-x;return res;}
void cdq(int l,int r){
	if(l==r)  return;
	int mid=(l+r)>>1;
	cdq(l,mid),sort(s+l,s+mid+1,cmp2);
	sort(s+mid+1,s+r+1,cmp2),cnt=0;
	for(int i=l;i<=r;i++)  rk[++cnt]=tmp[i]=s[i].t-s[i].x-s[i].y;
	sort(rk+1,rk+cnt+1),cnt=unique(rk+1,rk+cnt+1)-rk-1,fill(tr+1,tr+cnt+1,-INF);
	for(int i=l;i<=r;i++)  wz[i]=lower_bound(rk+1,rk+cnt+1,tmp[i])-rk;
	for(int i=l,j=mid+1;i!=mid+1||j!=r+1;){
		if(i!=mid+1&&(j==r+1||s[i].x<=s[j].x))  update(wz[i],dp[s[i].i]),i++;
		else  dp[s[j].i]=max(dp[s[j].i],query(wz[j])+s[j].a),j++;
	}
	reverse(s+l,s+mid+1),reverse(s+mid+1,s+r+1),cnt=0;
	for(int i=l;i<=r;i++)  rk[++cnt]=tmp[i]=s[i].t+s[i].x-s[i].y;
	sort(rk+1,rk+cnt+1),cnt=unique(rk+1,rk+cnt+1)-rk-1,fill(tr+1,tr+cnt+1,-INF);
	for(int i=l;i<=r;i++)  wz[i]=lower_bound(rk+1,rk+cnt+1,tmp[i])-rk;
	for(int i=l,j=mid+1;i!=mid+1||j!=r+1;){
		if(i!=mid+1&&(j==r+1||s[i].x>=s[j].x))  update(wz[i],dp[s[i].i]),i++;
		else  dp[s[j].i]=max(dp[s[j].i],query(wz[j])+s[j].a),j++;
	}
	sort(s+mid+1,s+r+1,cmp1),cdq(mid+1,r);
}
signed main(){
	scanf("%lld",&n);
	for(int i=1,t,x,y,a;i<=n;i++){
		scanf("%lld%lld%lld%lld",&t,&x,&y,&a);
		s[i]={t,x,y,a,i};
	}
	memset(dp,-0x3f,sizeof dp);
	sort(s+1,s+n+1,cmp1),cdq(dp[0]=0,n);
	for(int i=0;i<=n;i++)  ans=max(ans,dp[i]);
	printf("%lld\n",ans);
}

[UVA10304] Optimal Binary Search Tree

很简单的一道,深度可以看作祖先个数。

每个点的贡献就可以在自己和非根祖先处各统计一次,相当于统计所有非根点的子树和。

一个简单的区间 DP,设 \(dp_{l,r}\) 表示 \([l,r]\) 构成的树的答案,令 \(l>r\)\(dp_{l,r}=0\)\(sum_{l,r}=\sum_{i=l}^ra_i\)

有转移:\(dp_{l,r}=\min_{k=l}^{r}dp_{l,k-1}+dp_{k+1,r}+sum_{i,j}-a_k\)

表示枚举 \(k\) 为根,那就是两个子树的答案和 \(dp_{l,k-1}+dp_{k+1,r}\) 加上产生的贡献 \(sum_{i,j}-a_k\)

贡献这样计算是因为两棵子树的根都要贡献一次子树和,相当于区间所有除根以外数的和。

发现这个转移式子满足四边形不等式,用四边形不等式即可优化到 \(O(n^2)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
int n,a[N],sum[N],dp[N][N],p[N][N];
signed main(){
	while(~scanf("%lld",&n)){
		for(int i=1;i<=n;i++)  scanf("%lld",a+i),sum[i]=sum[i-1]+a[i];;
        for(int i=1;i<=n;i++)  fill(p[i],p[i]+n+1,0);
        for(int i=1;i<=n;i++)  fill(dp[i],dp[i]+n+1,INF);
		for(int i=1;i<=n;i++)  dp[i][i]=dp[i][i-1]=0,p[i][i]=i;
		for(int len=2;len<=n;len++)
			for(int i=1,j=len;j<=n;i++,j++)
				for(int k=p[i][j-1];k<=p[i+1][j];k++)
					if(dp[i][k-1]+dp[k+1][j]+sum[j]-sum[i-1]-a[k]<dp[i][j])
                        dp[i][j]=dp[i][k-1]+dp[k+1][j]+sum[j]-sum[i-1]-a[k],p[i][j]=k;
		printf("%lld\n",dp[1][n]);
	}
}

[ARC150F] Constant Sum Subsequence

\(f_i\) 表示所有以 \(a_i\) 结尾的序列的最小 \(sum\),有 \(f_i=a_i+\min_{j=lst_i}^{i-1}f_j\),其中 \(lst_i\)表示 \(a_i\) 的上一个出现位置。

答案就是满足 \(f_i=m\) 的最大的 \(i\) 值。

然后呢这个 \(f_i\) 可以用线段树维护转移,有结论为:当 \(i\ge n\times 2+1\) 时,\(f_{i+n}-f_i=f_i-f_{i-n}\)

感性理解就是在循环中的决策点理论上也应该是相同的,然后对于后面的我们直接做除法算值就可以了,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 4500005
#define int long long
int n,m,ans,a[N],dp[N],lst[N];
struct Segment_tree{
    int tr[N<<2];
    void update(int pos,int val,int l=0,int r=n*3,int p=1){
        if(l==r)  return tr[p]=val,void();
        int mid=(l+r)>>1,ls=p<<1,rs=p<<1|1;
        pos<=mid?update(pos,val,l,mid,ls):update(pos,val,mid+1,r,rs);
        tr[p]=min(tr[ls],tr[rs]);
    }
    int query(int sl,int sr,int l=0,int r=n*3,int p=1){
        if(sl<=l&&r<=sr)  return tr[p];
        int mid=(l+r)>>1,ls=p<<1,rs=p<<1|1;
        if(sr<=mid)  return query(sl,sr,l,mid,ls);
        if(sl>mid)  return query(sl,sr,mid+1,r,rs);
        return min(query(sl,sr,l,mid,ls),query(sl,sr,mid+1,r,rs));
    }
}SGT;
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)  scanf("%lld",a+i),a[i+n+n]=a[i+n]=a[i];
    for(int i=1;i<=n*3;i++){
        dp[i]=a[i]+SGT.query(lst[a[i]],i-1);
        SGT.update(i,dp[i]),lst[a[i]]=i;
        if(dp[i]==m)  ans=max(ans,i);
    }
    for(int i=1;i<=n;i++){
		int tmp=dp[n+n+i]-dp[n+i];
        if(m>dp[n+n+i]&&(m-dp[n+n+i])%tmp==0)  ans=max(ans,(m-dp[n+n+i])/tmp*n+n+n+i);
	}
    printf("%lld\n",ans);
}

8.14 DP杂题

[ARC125F] Tree Degree Subset Sum

首先发现树除了限制 \(\sum{d_i}=n\times 2-2\) 外没有任何作用。

然后一眼是所有多项式 \(x^{d_i}y+1\) 的卷积项数,发现所有 \(d_i\geq 1\),所以全部减 \(1\) 均衡掉 \(y\) 的幂次。

打表后大眼观察到所有 \(x^i\) 对应的 \(y\) 幂次是一段连续的数,考虑证明这个结论。

\(l_i\)\(r_i\) 分别表示 \(x^i\) 对应的 \(y\) 幂次的最小和最大值,假设有 \(s\)\(d_i=0\)

\([l_i,l_i+z]\)\([r_i-z,r_i]\) 都是一定可以得到的,所以考虑证明结论的充分条件 \(r_i-l_i\leq z\times 2\)

对于一个选择 \(d\) 的方案,设选取的 \(d\) 数量为 \(v\)\(\sum d\)\(w\),有 \(v-w\in [-z+2,z]\)

因为 \(v-w=\sum(-d_i+1)\),所以最大值显然是把 \(d_i=0\) 的数选满,即为 \(z\)

最小值是把所有 \(d_i\neq0\) 的数选满,即为 \((n-z)-(n-2)=-z+2\)

所以 \(l_x-k\)\(r_x-k\) 也在 \([-z+2,z]\) 中,\((r_x-l_x)_{max}=z-(-z+2)=z\times 2-2\leq z\times 2\),得证。

\(l_i\)\(r_i\) 的求解可以使用单调队列优化多重背包,因为 \(d_i\) 最多有 \(O(\sqrt{n})\) 种,所以复杂度 \(O(n\sqrt{n})\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define INF 0x3f3f3f3f
deque<int> dq;
long long ans;
int n,non,a[N],dp1[N],dp2[N],tmp[N],sum[N];
int main(){
    scanf("%d",&n);
    fill(dp1,dp1+n+1,INF),fill(dp2,dp2+n+1,-INF),dp1[0]=dp2[0]=0;
    for(int i=1,x,y;i<n;i++)  scanf("%d%d",&x,&y),a[x]++,a[y]++;
    for(int i=1;i<=n;i++)  non+=(!--a[i]),sum[a[i]]++;
    for(int i=1;i<=n;i++){
        if(!sum[i])  continue;
        for(int j=0;j<i;j++){
            while(!dq.empty())  dq.pop_back();
            for(int k=j;k<=n;k+=i)  tmp[k]=dp1[k];
            for(int k=j;k<=n;k+=i){
                while(!dq.empty()&&k-dq.front()>sum[i]*i)  dq.pop_front();
                if(!dq.empty())  tmp[k]=min(tmp[k],dp1[dq.front()]+(k-dq.front())/i);
                while(!dq.empty()&&dp1[dq.back()]-dq.back()/i>dp1[k]-k/i)  dq.pop_back();
                dq.push_back(k);
            }
            for(int k=j;k<=n;k+=i)  dp1[k]=tmp[k];
        }
        for(int j=0;j<i;j++){
            while(!dq.empty())  dq.pop_back();
            for(int k=j;k<=n;k+=i)  tmp[k]=dp2[k];
            for(int k=j;k<=n;k+=i){
                while(!dq.empty()&&k-dq.front()>sum[i]*i)  dq.pop_front();
                if(!dq.empty())  tmp[k]=max(tmp[k],dp2[dq.front()]+(k-dq.front())/i);
                while(!dq.empty()&&dp2[dq.back()]-dq.back()/i<dp2[k]-k/i)  dq.pop_back();
                dq.push_back(k);
            }
            for(int k=j;k<=n;k+=i)  dp2[k]=tmp[k];
        }
    }
    for(int i=0;i<=n;i++)  if(dp2[i]>=dp1[i])  ans+=dp2[i]-dp1[i]+non+1;
    printf("%lld\n",ans);
}

[CF1152F2] Neko Rules the Catniverse

发现两个 Version 的差别只有 \(n\) 的范围,所以考虑按值域 DP。

\(f_{i,j}\) 表示值域上填到 \(i\),一共填了 \(j\) 个数的方案数。

因为转移还要求 \(i\) 的前一个数要大于等于 \(i-m\) 或置于开头,而 \(m\) 很小。

考虑开设一维表示大于等于 \(i-m\) 的数放了哪些,因为我们才填到 \(i\),需要维护的就只有 \(m\) 个数,所以可以状压。

转移只分两种:填 \(i\) 和不填 \(i\)

对于填 \(i\),大于等于 \(i-m\) 的数有 \(k\) 个就有 \(k\) 个位置后面可以填。

\(popcount(s)\) 表示 \(s\) 中的 \(1\) 数量,有 \(f_{i,j,s}\times(popcount(s)+1)\rightarrow f_{i+1,j+1,(2s+1)\&(2^m-1)}\)

不填 \(i\) 就很简单,直接 \(f_{i,j,s}\rightarrow f_{i+1,j+1,(2s)\&(2^m-1)}\)

\(j\)\(s\) 一起哈希成一维就可以矩阵加速了,复杂度 \(O((2^mk)^3\log n)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define M 13*17+1
#define int long long
int n,k,m,p;
const int mod=1000000007;
struct matrix{
    int r,c,a[M][M];
    matrix(){memset(a,0,sizeof a);}
    int* operator[](size_t x){return a[x];}
    friend matrix operator*(matrix x,matrix y){
        matrix res;
        for(int i=0;i<M;i++)
            for(int j=0;j<M;j++)
                for(int k=0;k<M;k++)
                    res[i][j]=(res[i][j]+x[i][k]*y[k][j])%mod;
        return res;
    }
}st,mtx;
void quick_pow(matrix &st,matrix mtx,int x){
	for(;x;mtx=mtx*mtx,x>>=1)  if(x&1)  st=st*mtx;
}
int get(int x,int y){return x*p+y;}
signed main(){
    scanf("%lld%lld%lld",&n,&k,&m),p=1<<m,st[0][get(0,0)]=1;
	for(int j=0;j<k;j++){
		for(int s=0;s<p;s++){
			mtx[get(j,s)][get(j,(s<<1)&(p-1))]=1;
			mtx[get(j,s)][get(j+1,(s<<1|1)&(p-1))]=__builtin_popcount(s)+1;
		}
	}
	for(int s=0;s<p;s++)  mtx[get(k,s)][(k+1)*p]=1;
	mtx[(k+1)*p][(k+1)*p]=1,quick_pow(st,mtx,n+1),printf("%lld\n",st[0][(k+1)*p]);
}

[CF1242C] Sum Balance

首先 \(\sum a_{i,j}\) 不整除 \(k\) 的时候一定无解,否则我们就知道每个盒子需要的 \(a_{i,j}\) 和是多少。

考虑每个背包提取出一个数后,需要加进来的数也是固定的。

所以我们连 \(u\)\(v\) 的边,表示一个盒子提取出 \(u\) 后需要放入 \(v\)

然后题目变成了能否选取任意个环,使得每个盒子都有且仅有一个数出现在了环上。

因为 \(a_{i,j}\) 互不相同,所以每个数至多有一条出边,基环树找环是容易的。

又发现 \(k\) 极小,考虑状压 DP,记录一下前驱节点和转移时用的环。

然后这个题就愉快地做完了,因为要子集枚举,所以复杂度 \(O(nk+3^k)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define K 25
#define N 5005
#define int long long
pair<int,int> pre[1<<15];
vector<tuple<int,int,int>> ans[1<<15];
int vis[N*K],fbl[K],dp[1<<15],ans1[K],ans2[K];
int k,cnt,sum,n[K],a[K][N],rk[N*K],nxt[N*K],w[N*K],bl[N*K];
void dfs(int x){
    if(!x)  return;
    for(auto [a,b,c]:ans[x])  ans1[a]=b,ans2[a]=c;
    dfs(pre[x].first),dfs(pre[x].second);
}
signed main(){
    scanf("%lld",&k),dp[0]=1;
    for(int i=1;i<=k;i++){
        scanf("%lld",n+i);
        for(int j=1;j<=n[i];j++)  scanf("%lld",a[i]+j);
        for(int j=1;j<=n[i];j++)  sum+=a[i][j];
        for(int j=1;j<=n[i];j++)  rk[++cnt]=a[i][j];
    }
    if(sum%k!=0)  return printf("No\n"),0;
    sort(rk+1,rk+cnt+1),sum/=k;
    for(int i=1;i<=k;i++){
        int tot=0;
        for(int j=1;j<=n[i];j++)  tot+=a[i][j];
        for(int j=1;j<=n[i];j++){
            int now=lower_bound(rk+1,rk+cnt+1,a[i][j])-rk;
            int nxt=lower_bound(rk+1,rk+cnt+1,sum-(tot-a[i][j]))-rk;
            if(rk[nxt]!=sum-(tot-a[i][j]))  continue;
            ::nxt[now]=nxt,w[now]=a[i][j],bl[now]=i;
        }
    }
    for(int i=1;i<=cnt;i++){
        int j=i;
        while(!vis[j]&&nxt[j])  vis[j]=i,j=nxt[j];
        if(vis[j]!=i||!nxt[j])  continue;
        int k=j,s=0,pre=j;memset(fbl,0,sizeof fbl);
        vector<tuple<int,int,int>> tra(0,make_tuple(0,0,0));
        do{
            pre=k,k=nxt[k];
            if(fbl[bl[k]])  goto cant;
            fbl[bl[k]]=1,s|=1<<(bl[k]-1);
            tra.emplace_back(bl[k],w[k],bl[pre]);
        }while(k!=j);
        dp[s]=1,::pre[s]={0,0},ans[s]=tra;cant:;
    }
    for(int j=0;j<(1<<k);j++){
        if(dp[j])  continue;
        for(int k=j;k;k=(k-1)&j)
            if(dp[k]&&dp[j^k]){dp[j]=1,pre[j]={k,j^k};break;}
    }
    if(!dp[(1<<k)-1])  return printf("No\n"),0;
    printf("Yes\n");int now=(1<<k)-1;dfs(now);
    for(int i=1;i<=k;i++)  printf("%lld %lld\n",ans1[i],ans2[i]);
}

[SMOI-R2] Monotonic Queue

向队列里加入一个数时会让它前面直到第一个 \(a_i\) 大于等于它的数全部产生贡献。

考虑到这个性质直接用单调栈(只有队尾的队列)维护值,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 500005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
int n,head=1,tail,ans=-INF,a[N],q[N],sum[N],dp[N];
signed main(){
	scanf("%lld",&n),a[0]=n+1;
	for(int i=1;i<=n;i++)  scanf("%lld",sum+i),sum[i]+=sum[i-1];
	for(int i=1;i<=n;i++)  scanf("%lld",a+i);
	q[++tail]=0,memset(dp,-0x3f,sizeof dp),dp[0]=0;
	for(int i=1;i<=n;i++){
		while(a[q[tail]]<a[i])  dp[i]=max(dp[i],sum[i-1]-sum[q[tail]-1]+dp[q[tail]]),tail--;
		dp[i]=max(dp[i],sum[i-1]-sum[q[tail]]+dp[q[tail]]),q[++tail]=i,ans=max(ans,dp[i]);
	}
    printf("%lld\n",ans);
}

[NOI2011] NOI 嘉年华

由于时间没什么大用处,所以首先考虑离散化,设离散化后有 \(m\) 个时间。

\(cnt_{i,j}\) 表示 \([i,j]\) 内的活动总数,这是好计算的。

\(pre_{i,j}\) 表示 \([1,i]\) 内其中一边选了 \(j\) 个活动,另一边最多能选多少个活动。

转移考虑枚举 \([k,i]\) 的一块:\(pre_{i,j}=\max_{k=1}^i\{pre_{k,j}+tot_{k,i},pre_{k,j-tot_{k,j}}\}\)

第一问的答案即为 \(\max_{i=1}^n\{\min(i,pre_{m,i})\}\)

然后思考怎么求第二问的答案,介于强制选 \(i\),所以理论上还需要一个后缀数组。

考虑求一个转移类似的 \(suf_{i,j}\),表示 \([i,m]\) 内其中一边选了 \(j\) 个活动,另一边最多能选多少个活动。

此时 \([s_i,t_i]\) 的活动一定被一边选走了,设 \(f_{i,j}\) 表示 \([i,j]\) 强制被一边选走的答案。

设再在前面选 \(x\) 个,后面选 \(y\) 个,有 \(f_{i,j}=\max_{x=1}^m\max_{y=1}^m\{\min(x+cnt_{i,j}+y,pre_{l,x}+suf_{r,y})\}\)

因为可能有区间跨过 \([s_i,t_i]\),所以 \(ans_i=\max_{l=1,r=t_i}^{l\leq s_i,r\leq m}\{f_{l,r}\}\)

这样的话复杂度是 \(O(n^4)\) 的,尽管已经可以通过,但我们还是考虑进行优化。

发现若 \(x\)\(y\) 同时增大,那 \(pre_{l,x}\)\(suf_{r,y}\) 会同时减小,就不会对 \(f_{i,j}\) 产生贡献。

所以考虑双指针,一增一减,复杂度降为 \(O(n^3)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 405
#define INF 0x3f3f3f3f
struct Node{int l,r;}a[N];
int n,tot,ans=-INF,rk[N],cnt[N][N],pre[N][N],suf[N][N],dp[N][N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)  scanf("%d%d",&a[i].l,&a[i].r),a[i].r+=a[i].l-1;
    for(int i=1;i<=n;i++)  rk[++tot]=a[i].l,rk[++tot]=a[i].r;
    sort(rk+1,rk+tot+1),tot=unique(rk+1,rk+tot+1)-rk-1;
    for(int i=1;i<=n;i++)  a[i].l=lower_bound(rk+1,rk+tot+1,a[i].l)-rk;
    for(int i=1;i<=n;i++)  a[i].r=lower_bound(rk+1,rk+tot+1,a[i].r)-rk;
    for(int i=1;i<=n;i++)  cnt[a[i].l][a[i].r]++;
    for(int i=1;i<=tot;i++)  for(int j=i+1;j<=tot;j++)  cnt[i][j]+=cnt[i][j-1];
    for(int j=tot;j>=1;j--)  for(int i=j-1;i>=1;i--)  cnt[i][j]+=cnt[i+1][j];
    for(int i=0;i<=tot;i++)  fill(pre[i],pre[i]+n+1,-INF);
	pre[0][0]=0;
    for(int i=1;i<=tot;i++){
        for(int j=0;j<=cnt[1][i];j++){
            for(int k=0;k<i;k++){
                pre[i][j]=max(pre[i][j],pre[k][j]+cnt[k+1][i]);
                if(j>=cnt[k+1][i])  pre[i][j]=max(pre[i][j],pre[k][j-cnt[k+1][i]]);
            }
        }
    }     
    for(int i=0;i<=n;i++)  ans=max(ans,min(pre[tot][i],i));
    printf("%d\n",ans);
    for(int i=1;i<=tot+1;i++)  fill(suf[i],suf[i]+n+1,-INF);
	suf[tot+1][0]=0;
    for(int i=tot;i>=1;i--){
        for(int j=0;j<=n;j++){
            for(int k=i+1;k<=tot+1;k++){
                suf[i][j]=max(suf[i][j],suf[k][j]+cnt[i][k-1]);
                suf[i][j]=max(suf[i][j],suf[k][max(0,j-cnt[i][k-1])]);
            }
        }
    }
	for(int l=1;l<=tot;l++){
        for(int r=l+1;r<=tot;r++){
            dp[l][r]=-INF;
            auto calc=[&](int i,int j){return min(i+cnt[l][r]+j,pre[l-1][i]+suf[r+1][j]);};
            for(int i=0,j=n;i<=n;i++){
                while(j&&calc(i,j-1)>=calc(i,j))  j--;
                dp[l][r]=max(dp[l][r],calc(i,j));
            }
        }
    }
    for(int i=1;i<=tot;i++)  for(int j=tot;j>i;j--)  dp[i][j-1]=max(dp[i][j-1],dp[i][j]);
    for(int j=tot;j>=1;j--)  for(int i=1;i<j;i++)  dp[i+1][j]=max(dp[i+1][j],dp[i][j]);
    for(int i=1;i<=n;i++)  printf("%d\n",dp[a[i].l][a[i].r]);
}

[SMOI-R2] Speaker

\(f_i\) 表示所有节点中 \(a_x\) 减去 \(i\)\(x\) 的路径两倍长的最大值。

容易得出答案为 \(a_x+a_y\) 减去路径和再加上路径上的最大 \(f\)

求出 \(f\) 后剩下的所有值运用树剖或倍增 LCA 都是好统计的,所以考虑如何计算 \(f\)

然后你发现这个转移很水 \(f_i=\max(a_x,\max_{y\in son_x}\{f_y-w_{x,y}\times 2\})\)

就是如果要从子树转移就要减去两倍的这条边,换根一下就能求出最终的 \(f\)

再然后,这个题就啥都没了,复杂度 \(O((n+q)\log n)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define int long long
vector<int> G[N];
int n,q,a[N],fr[N],to[N],val[N];
struct ST{
	int lg[N],mx[19][N];
	void init(){
		for(int i=2;i<=n;i++)  lg[i]=lg[i>>1]+1;
		for(int k=1;k<=lg[n];k++)
			for(int i=1;i+(1<<k)-1<=n;i++)
				mx[k][i]=max(mx[k-1][i],mx[k-1][i+(1<<(k-1))]);
	}
	int query(int l,int r){
		int k=lg[r-l+1];
		return max(mx[k][l],mx[k][r-(1<<k)+1]);
	}
}ST;
int dfncnt,dfn[N],fa[N],dep[N],sz[N],son[N],top[N],dis[N],dp[N];
void dfs(int u){
	dp[u]=a[u],sz[u]=1;
	for(int e:G[u]){
		int v=fr[e]^to[e]^u;
		if(v==fa[u])  continue;
		fa[v]=u,dep[v]=dep[u]+1,dis[v]=dis[u]+val[e];
		dfs(v),dp[u]=max(dp[u],dp[v]-(val[e]<<1)),sz[u]+=sz[v];
		if(sz[v]>sz[son[u]])  son[u]=v;
	}
}
void gettop(int u,int top){
	::top[u]=top,dfn[u]=++dfncnt;
	if(son[u])  gettop(son[u],top);
	for(int e:G[u]){
		int v=fr[e]^to[e]^u;
		if(v==fa[u]||v==son[u])  continue;
		gettop(v,v);
	}
}
void getdp(int u){
	for(int e:G[u]){
		int v=fr[e]^to[e]^u;
		if(v==fa[u])  continue;
		dp[v]=max(dp[v],dp[u]-(val[e]<<1)),getdp(v);
	}
}
int LCA(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])  swap(u,v);
		u=fa[top[u]];
	}
	return dep[u]<dep[v]?u:v;
}
int query(int u,int v){
	int res=0;
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]])  swap(u,v);
		res=max(res,ST.query(dfn[top[u]],dfn[u])),u=fa[top[u]];
	}
	if(dep[u]>dep[v])  swap(u,v);
	return max(res,ST.query(dfn[u],dfn[v]));
}
signed main(){
	scanf("%lld%lld",&n,&q);
	for(int i=1;i<=n;i++)  scanf("%lld",a+i);
	for(int i=1;i<n;i++){
		scanf("%lld%lld%lld",fr+i,to+i,val+i);
		G[fr[i]].emplace_back(i),G[to[i]].emplace_back(i);
	}
	dfs(1),gettop(1,1),getdp(1);
	for(int i=1;i<=n;i++)  ST.mx[0][dfn[i]]=dp[i];
	ST.init();
	for(int i=1,u,v;i<=q;i++){
		scanf("%lld%lld",&u,&v);int lca=LCA(u,v);
		printf("%lld\n",max(query(u,v),dp[lca])+a[u]+a[v]-dis[u]-dis[v]+(dis[lca]<<1));
	}
}

8.15 DP杂题

[JROI-4] 沈阳大街 2

实际上 \(A\)\(B\) 的条件限制并没有任何作用,因为任意两个数组可以通过交换 \(A\)\(B\) 和数组排序等方式变成这个样子。

本质上这个题是求 \(A\)\(B\) 的所有匹配贡献,一个匹配的贡献是 \(\prod\min(A_i,B_i)\)

考虑将 \(A\)\(B\) 合并为 \(C\) 从大到小排序并用颜色进行区分,方便统计每个数对于答案的贡献。

\(f_{i,j}\) 表示考虑 \(C\) 的前 \(i\) 个数,匹配了 \(j\) 对的方案数,设 \(sum_{i,0/1}\) 表示前 \(i\) 个数中对应颜色有多少个。

\(f_{i,j}\) 转移分为匹配和不匹配,不匹配的转移显然 \(f_{i-1,j}\rightarrow f_{i,j}\)

对于匹配,前面会有 \(sum_{i,!col_i}-(j-1)\) 个数可以进行匹配,所以 \(f_{i-1,j-1}\times (sum_{i,!col_i}-(j-1))\times C_i\rightarrow f_{i,j}\)

答案即为 \(f_{n\times 2,n}\),时间复杂度 \(O(n^2)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 10005
#define int long long
const int mod=998244353;
struct Node{int col,val;}c[N];
int n,now=1,pre=0,pri=1,dp[2][N],sum[N];
int quick_pow(int x,int y,int res=1){
    for(;y;x=x*x%mod,y>>=1)  if(y&1)  res=res*x%mod;
    return res;
}
signed main(){
    scanf("%lld",&n),dp[pre][0]=1;
    for(int i=1;i<=n;i++){
        c[i].col=0;
        scanf("%lld",&c[i].val);
    }
    for(int i=n+1;i<=n+n;i++){
        c[i].col=1;
        scanf("%lld",&c[i].val);
    }
    sort(c+1,c+n+n+1,[](Node a,Node b){return a.val>b.val;});
    for(int i=1;i<=n*2;i++,swap(pre,now)){
        sum[c[i].col]++;
        for(int j=0;j<=i/2;j++){
            dp[now][j]=dp[pre][j];int tmp=max(0ll,sum[!c[i].col]-(j-1));
            if(j)  dp[now][j]=(dp[now][j]+dp[pre][j-1]*tmp%mod*c[i].val)%mod;
        }
    }
    for(int i=1;i<=n;i++)  pri=pri*i%mod;
    printf("%lld\n",quick_pow(pri,mod-2)*dp[pre][n]%mod);
}

[CF2115C] Gellyfish and Eternal Violet

我们考虑什么时候会选择攻击。

发现当满足对于所有 \(a_i\) 都满足 \(a_i>1\),闪耀攻击一定会被执行,当存在两个 \(a_i\) 不相等,非闪耀攻击一定会被执行。

只有当所有 \(a_i\) 相同时才会考虑是否执行非闪耀攻击,因为有时一直执行闪耀攻击显然更优。

\(dp_{i,j,k}\) 表示还有 \(i\) 个回合,\(\min\{a_x\}=j\)\(\sum_{i=1}^n(a_x-\min\{a_x\})=k\) 时达成目标的概率。

\(k>0\) 时有唯一策略,此时 \(dp_{i,j,k}=(1-p)\times dp_{i-1,j,k-1}+p\times dp_{i-1,j-1,k}\)

否则要考虑两种情况,有 \(dp_{i,j,0}=(1-p)\times\max(dp_{i-1,j-1,n-1},dp_{i-1,j,0})+p\times dp_{i-1,j-1,0}\)

边界条件为 \(dp_{i,1,0}=1\),这个转移是 \(O(nmV^2)\) 的,所以需要考虑优化。

发现当 \(k\) 第一次变为 \(0\) 之前所有策略是唯一确定的,且第一次变为 \(0\) 后就不会超过 \(n-1\)

然后就可以考虑枚举第 \(i\) 后变为 \(0\),答案即为 \((1-p)^k\times p^{i-k}\times\binom{i-1}{k-1}\times dp_{m-i,\max(j-(i-k),1),0}\)

这样第三维的大小就降为 \(n\) 了,但组合数太大会丢精度,设 \(f_{i,j}\) 表示做了 \(i\) 次操作后 \(k\) 的值为 \(j\),且过程没变成 \(0\) 的概率。

\(f\) 是容易计算的,答案等于 \(\sum_{i=k}^mf_{i,0}\times dp_{m-i,\max(j-(i-k),1),0}\),时间复杂度 \(O(nmV)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 405
#define INF 0x3f3f3f3f
int t,n,m,s,mn,a[N];
double p,ans,dp[4005][N][25],f[8005];
int main(){
	scanf("%d",&t);
	for(int i=0;i<=4000;i++)  dp[i][1][0]=1;
	while(t--){
		scanf("%d%d%lf",&n,&m,&p),p/=100,mn=INF,s=0;
		for(int i=1;i<=n;i++)  scanf("%d",a+i);
		for(int i=1;i<=n;i++)  mn=min(mn,a[i]);
		for(int i=1;i<=n;i++)  s+=a[i]-mn;
		for(int i=1;i<=m;i++){
			for(int j=1;j<=mn;j++){
				if(j!=1)  dp[i][j][0]=(1-p)*max(dp[i-1][j-1][n-1],dp[i-1][j][0])+p*dp[i-1][j-1][0];
				for(int k=1;k<n;k++)  dp[i][j][k]=(1-p)*dp[i-1][j][k-1]+p*dp[i-1][j-(j!=1)][k];
			}
		}
		if(s<n){printf("%.9lf\n",dp[m][mn][s]);continue;}
        fill(f,f+s+2,0),f[s]=1,ans=0;
		for(int i=1;i<=m;i++){
            if(i>=s)  ans+=(1-p)*f[1]*dp[m-i][max(mn-(i-s),1)][0];
            for(int j=1;j<=s;j++)  f[j]=(1-p)*f[j+1]+p*f[j];
        }
        printf("%.9lf\n",ans);
	}
}

[OOI 2023] Another n-dimensional chocolate bar

\(f_{i,j}\) 表示 \(b_1\)\(b_i\) 已经确定,且 \(\prod_{k=j+1}^n b_p\geq j\) 的前提下,\(\prod_{k=1}^i\lfloor \frac{a_k}{b_k}\rfloor \times \frac{1}{a_k}\) 的最大值。

考虑通过枚举 \(b_i\) 转移,令 \(b_i=x\),有 \(f_{i,\lceil \frac{j}{x} \rceil}\leftarrow f_{i-1,j}\times \lfloor \frac{a_i}{x} \rfloor\times \frac{1}{a_i}\)

边界条件:\(f_{0,i}=1\),答案即为 \(f_{n,1}\times k\),时间复杂度是 \(O(nk)\) 的,所以考虑优化。

考虑将式子中的向上取整统一转为向下取整,有 \(\lceil \frac{k}{a} \rceil=\lfloor \frac{k-1}{a}\rfloor+1\)

接下来就可以整除分块去掉无用状态,并给予状态编号减小空间复杂度,最后的时间复杂度据说是 \(O(nk^{0.75})\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define M 105
#define N 10000005
double dp[M][10005];
int n,k,cnt,a[M],rk[N],dy[N];
int main(){
	scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)  scanf("%d",a+i);
	for(int i=1,j;i<k;i=j+1){
		rk[++cnt]=(k-1)/i;
		j=(k-1)/((k-1)/i);
	}
	rk[++cnt]=0,reverse(rk+1,rk+1+cnt),dp[0][cnt]=1;
	for(int i=1;i<=cnt;i++)  dy[rk[i]]=i;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=cnt;j++){
			if(!dp[i-1][j])  continue;
			for(int l=1,r;l<=rk[j];l=r+1){
				r=rk[j]/(rk[j]/l);
				dp[i][dy[rk[j]/l]]=max(dp[i-1][j]*(a[i]/l)/a[i],dp[i][dy[rk[j]/l]]);
			}
			dp[i][1]=max(dp[i][1],dp[i-1][j]*(a[i]/(rk[j]+1))/a[i]);
		}
	}
	printf("%.10lf\n",dp[n][1]*k);
}

[CF2125F] Timofey and Docker

因为 docker 没有相同的真前缀和真后缀,所以所有的 docker 都是独立出现的,设初始有 \(x\) 个 docker。

然后我们减少 \(x\) 一定是减少到比 \(x\) 小的最大的可选项,增加 \(x\) 一定是增加到比 \(x\) 大的最小的可选项。

设减少到 \(y\),那将 \(x-y\) 个 docker 的其中一个字符改掉就行了,答案就是 \(x-y\),增加到 \(y\) 则考虑一个 \(O(n^2)\) 的 DP。

\(p_i\) 表示将 \(s_{i}\)\(s_{i+5}\) 改成 docker 的最小操作次数,\(dp_{i,j}\) 表示让 \(s_1\)\(s_i\)\(j\) 个 docker 的答案。

有简单转移:\(dp_{i,j}=\min(dp_{i-1,j},dp_{i-5,j-1}+p_{i-5})\),分别是无和有以 \(s_j\) 结尾的 docker。

然后发现这个 DP 的制约条件是有 \(j\) 个 docker,但我们需要的位置是确定的,考虑 WQS 二分。

令更改一个数的贡献为 \(-1\),获得一个 docker 的贡献为 \(mid\)\(f_i\) 表示只考虑 \(s_1\)\(s_i\) 的最大贡献。

有简单转移:\(f_i=\max(f_{i-1},f_{i-6}+mid-p_{i-5})\),额外记录一下 docker 数即可。

设二分出的贡献为 \(k\),则答案为 \(-(dp_m-ky)\),实现复杂度 \(O(n\log n)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 500005
#define int long long
#define INF 0x3f3f3f3f
char ch[N];
int t,n,m,l[N],r[N],p[N],sum[N],dp[N],doc[N];
int check(int mid){
	fill(dp,dp+m+1,0);
	for(int i=1;i<=m;i++){
		dp[i]=dp[i-1],doc[i]=doc[i-1];
		if(i>=6&&dp[i-6]+mid-p[i-5]>=dp[i])
			dp[i]=dp[i-6]+mid-p[i-5],doc[i]=doc[i-6]+1;
	}
	return doc[m];
}
signed main(){
	scanf("%lld",&t);
	while(t--){
		scanf("%s%lld",ch+1,&n),m=strlen(ch+1);
		for(int i=1;i<=n;i++)  scanf("%lld%lld",l+i,r+i);
		fill(sum,sum+m/6+1,0);
		for(int i=1;i<=n;i++){
			if(l[i]<=m/6)  sum[l[i]]++;
			if(r[i]<=m/6)  sum[r[i]+1]--;
		}
		for(int i=1;i<=m/6;i++)  sum[i]+=sum[i-1];
		int sl=-1,sr=-1,now=0,mx=0;
		for(int i=1;i<=m-5;i++){
			p[i]=0;
			if(ch[i]!='d')  p[i]++;
			if(ch[i+1]!='o')  p[i]++;
			if(ch[i+2]!='c')  p[i]++;
			if(ch[i+3]!='k')  p[i]++;
			if(ch[i+4]!='e')  p[i]++;
			if(ch[i+5]!='r')  p[i]++;
			now+=(!p[i]);
		}
		for(int i=1;i<=m/6;i++)  mx=max(mx,sum[i]);
		for(int i=now;i>=1;i--)  if(sum[i]==mx){sl=i;break;}
		for(int i=now;i<=m/6;i++)  if(sum[i]==mx){sr=i;break;}
		int ans1=(sl==-1?INF:now-sl);
		if(sr==-1){printf("%lld\n",ans1);continue;}
		int l=0,r=max(n,m);
		while(l<r){
			int mid=(l+r)>>1;
			check(mid)>=sr?r=mid:l=mid+1;
		}
		check(l),printf("%lld\n",min(ans1,-(dp[m]-l*sr)));
	}
}

[CF2034F] Khayyam's Royal Decree

这道题不想写,见这篇题解我的代码

8.16 NOIP模拟赛

题目链接

T1 可爱的数列(实际/期望:100/100)

最开始没看到区间不能相交,心想怎么放道橙题来,T1 也不至于这么简单吧。

虽然说看对题过后还是很简单的,直接按区间和做活动安排就行了,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2005
int n,ans,cnt,a[N];
vector<pair<int,int>> pri;
unordered_map<int,int> mp;
struct Node{int val,l,r;}s[N*N];
bool cmp(Node a,Node b){
    if(a.val!=b.val)  return a.val<b.val;
    if(a.r!=b.r)  return a.r<b.r;
    return a.l<b.l;
}
int main(){
    freopen("love.in","r",stdin);
    freopen("love.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)  scanf("%d",a+i);
    for(int i=1;i<=n;i++)
        for(int j=i,sum=0;j<=n;j++)
            s[++cnt]={sum+=a[j],i,j};
    sort(s+1,s+cnt+1,cmp);
    for(int i=1,j=1;i<=cnt;i=j){
        while(j<=cnt&&s[i].val==s[j].val)  j++;
        int now=0,sum=0;
        vector<pair<int,int>> tmp(0,make_pair(0,0));
        for(int k=i;k<j;k++){
            if(s[k].l<=now)  continue;
            sum++,now=s[k].r,tmp.emplace_back(s[k].l,s[k].r);
        }
        if(sum>ans)  ans=sum,pri=tmp;
    }
    printf("%d\n",ans);
    for(auto x:pri)  printf("%d %d\n",x.first,x.second);
}

T2 [POI 2009] GAS-Fire Extinguishers(实际/期望:40/20)

这道题,考场上想了个正确性和时间复杂度同时完全错误的贪心。

题解思路很简单,但是过不了原题小样例的情况下还过了 Becoder 数据。

在数据较为随机的情况下,如下代码有 50pts,甚至原题有 70pts:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,k;
int main(){
    scanf("%d%d%d",&n,&m,&k);
    int ans=n/m;
    if(n%m!=0)  ans++;
    printf("%d\n",ans)。
}

鉴于这个题过于神奇,我决定放弃编写本题的博客。

T3 [CEOI 2025] lawnmower(实际/期望:100/100)

首先有一个很容易想到的暴力 \(O(n^2)\) 做法,设 \(dp_{i,j}\) 表示扫描完 \(a_i\) 后,箱子里装了 \(j\) 数量垃圾的最小时间花费。

特别地,我们钦定垃圾余量不能为 \(c\) ,因为此时垃圾一定会被清空,这个状态是无意义的。

考虑 \(a_i\),设 \(sum\) 表示初始箱子余量为 \(0\) 是只考虑打扫完 \(a_i\) 至少需要多少时间。

\(a_i\)\(c\) 的倍数时,显然有 \(dp_{i-1,0} \rightarrow dp_{i,0}+sum\)

对于 \(1\leq j \leq n-1\),有 \(dp_{i-1,j}\rightarrow dp_{i,j}+sum+a_i\),因为相比与余量 \(0\),需要多向草坪跑一次清完最后一点。

\(a_i\) 不为 \(c\) 的倍数时,对于 \(0\leq j \leq c-(a_i\bmod c)-1\),有 \(dp_{i-1,j}\rightarrow dp_{i,j+(a_i \bmod c)}+sum\),因为清垃圾次数相同。

对于 \(j=c-(a_i\bmod c)\),有 \(dp_{i-1,j}\rightarrow dp_{i,0}+sum+b\),相比上一项要多清理一次垃圾。

对于 \(c-(a_i\bmod c)+1\leq j \leq c-1\),有 \(dp_{i-1,j}\rightarrow dp_{i,0}+sum+b+a_i\),相比上一项多跑一次草坪。

最后就是 \(\min\{dp_{i,j}\}+b\rightarrow dp_{i,0}\),因为任意容量都可以清空。

我们发现这个转移被分为了至多三段,于是可以动态偏移下标 \(0\) 的位置,再用上区间加,全局最小值和单点查询操作。

然后这些用线段树就可以全部做完啦,时间复杂度 \(O(n\log V)\),因为要动态开点,代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define int long long
int n,c,b,pos,a[N],v[N];
struct Segment_tree{
    int rt,cnt,tr[N<<6],tag[N<<6],ls[N<<6],rs[N<<6];
    int query(int pos,int l,int r,int p){
        if(!p)  return 0;
        int mid=(l+r)>>1,sum=0;
        if(pos<=mid)  sum=query(pos,l,mid,ls[p]);
        else  sum=query(pos,mid+1,r,rs[p]);
        return sum+tag[p];
    }
    void update(int pos,int x,int l,int r,int &p){
        if(!p)  p=++cnt;
        if(l==r)  return tr[p]=x,tag[p]=x,void();
        int mid=(l+r)>>1;
        if(pos<=mid)  update(pos,x-tag[p],l,mid,ls[p]);
        else  update(pos,x-tag[p],mid+1,r,rs[p]);
        if(!ls[p])  tr[p]=tr[rs[p]]+tag[p];
        else if(!rs[p])  tr[p]=tr[ls[p]]+tag[p];
        else  tr[p]=min(tr[ls[p]],tr[rs[p]])+tag[p];
    }
    void update(int sl,int sr,int x,int l,int r,int &p){
        if(!p)  p=++cnt;
        if(sl<=l&&r<=sr)  return tr[p]+=x,tag[p]+=x,void();
        int mid=(l+r)>>1;
        if(sl<=mid)  update(sl,sr,x,l,mid,ls[p]);
        if(sr>mid)  update(sl,sr,x,mid+1,r,rs[p]);
        if(!ls[p])  tr[p]=tr[rs[p]]+tag[p];
        else if(!rs[p])  tr[p]=tr[ls[p]]+tag[p];
        else  tr[p]=min(tr[ls[p]],tr[rs[p]])+tag[p];
    }
}SGT;
void update(int l,int r,int sum){
    if(l>r){
        SGT.update(l,c-1,sum,0,c-1,SGT.rt);
        SGT.update(0,r,sum,0,c-1,SGT.rt);
    }
    else  SGT.update(l,r,sum,0,c-1,SGT.rt);
}
int mow(signed n,signed c,signed b,vector<signed> &aa,vector<signed> &vv){
    ::n=n,::c=c,::b=b;
    for(int i=1;i<=n;i++)  a[i]=aa[i-1];
    for(int i=1;i<=n;i++)  v[i]=vv[i-1];
    for(int i=1;i<=n;i++){
        int ned=v[i]/c,hs=v[i]%c,sum=(ned+1)*a[i]+ned*b;
        if(!hs){
            update(pos,pos,sum-a[i]);
            if(c!=1)  update((pos+1)%c,(pos+c-1)%c,sum);
        }
        else{
            update(pos,(pos+c-1-hs)%c,sum);
            update((pos+c-hs)%c,(pos+c-hs)%c,sum+b);
            if(hs!=1)  update((pos+c-hs+1)%c,(pos+c-1)%c,sum+a[i]+b);
        }
        pos=(pos-hs+c)%c;
        int dp=SGT.query(pos,0,c-1,SGT.rt);
        if(SGT.tr[1]+b<dp)  SGT.update(pos,SGT.tr[1]+b,0,c-1,SGT.rt);
    }
    return min(SGT.query(pos,0,c-1,SGT.rt),SGT.tr[1]+b);
}

T4 套娃(实际/期望:60/60)

首先我们将所有 \(a_i\) 给加上 \(1\),然后对于一次询问,记 \(sum_i\)\(a\) 数组中 \(\leq i\) 的数的个数,答案为 \(\min\{\lceil\frac{cnt_i}{i}\rceil \}\)

于是有一个神仙做法,因为每次答案至多减少 \(1\),而只有 \(\leq\lfloor\frac{n}{nowans}\rfloor\) 的点能对答案产生贡献。

于是可以用线段树维护 \(sum_i\) 还有多久会导致答案增加,只需要区间减 \(1\) 就行了,然后答案更新的时候直接重构线段树。

最后你就可以发现这个复杂度是正确的,为 \(O(n\log n)\),代码如下:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int n,ans,a[N],s[N];
struct Segment_tree{
	int tr[N<<1],tag[N<<1];
	void build(int l,int r,int p){
		tag[p]=0;
		if(l==r)  return tr[p]=ans*l-s[l],void();
		int mid=(l+r)>>1,ls=mid<<1,rs=mid<<1|1;
		build(l,mid,ls),build(mid+1,r,rs);
		tr[p]=min(tr[ls],tr[rs]);
	}
	void pushdown(int p,int ls,int rs){
		tr[ls]+=tag[p],tag[ls]+=tag[p];
		tr[rs]+=tag[p],tag[rs]+=tag[p],tag[p]=0;
	}
	void update(int sl,int sr,int l,int r,int p){
		if(sl<=l&&r<=sr)  return tr[p]--,tag[p]--,void();
		int mid=(l+r)>>1,ls=mid<<1,rs=mid<<1|1;
		pushdown(p,ls,rs);
		if(sl<=mid)  update(sl,sr,l,mid,ls);
		if(sr>mid)  update(sl,sr,mid+1,r,rs);
		tr[p]=min(tr[ls],tr[rs]);
	}
	int query(int sl,int sr,int l,int r,int p){
		if(sl<=l&&r<=sr)  return tr[p];
		int mid=(l+r)>>1,ls=mid<<1,rs=mid<<1|1;
		pushdown(p,ls,rs);
		if(sr<=mid)  return query(sl,sr,l,mid,ls);
		if(sl>mid)  return query(sl,sr,mid+1,r,rs);
		return min(query(sl,sr,l,mid,ls),query(sl,sr,mid+1,r,rs));
	}
}SGT;
int main(){
	freopen("doll.in","r",stdin);
	freopen("doll.out","w",stdout);
	scanf("%d",&n),ans=1,SGT.build(1,n,1);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x),a[x+1]++;
		if(x+1<=n/ans&&SGT.query(x+1,n/ans,1,n/ans,1)<=0){
			for(int j=1;j<=n/(ans+1);j++)  s[j]=s[j-1]+a[j];
			ans++,SGT.build(1,n/ans,1);
		}
		else if(x+1<=n/ans)  SGT.update(x+1,n/ans,1,n/ans,1);
		printf("%d ",ans);
	}
}

模拟赛总结

期望:100+20+100+60=280,实际:100+40+100+60=300。

得分勉强达到目标,机房 rk4,算考得比较正常,至少没莫名其妙挂分。

posted @ 2025-08-11 19:03  tkdqmx  阅读(135)  评论(6)    收藏  举报