新高一暑假二期集训 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\),则:
所以可以使用决策单调性优化,复杂度 \(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,算考得比较正常,至少没莫名其妙挂分。

浙公网安备 33010602011771号