3.动态规划

省选动态规划专题

开题顺序: \(ATKMBQRDCEFGSJOIPN\)

\(A\) luogu P4141 消失之物

  • 题解

    点击查看代码
    int w[2001],v[2001],f[2001],g[2001];
    int main()
    {
        int n,m,i,j;
        cin>>n>>m;
        f[0]=1;
        for(i=1;i<=n;i++)
        {
            cin>>w[i];
        }
        for(i=1;i<=n;i++)
        {
            for(j=m;j>=w[i];j--)
            {
                f[j]=(f[j]+f[j-w[i]])%10;
            }
        }
        for(i=1;i<=n;i++)
        {
            g[0]=1;
            for(j=1;j<=m;j++)
            {
                if(j-w[i]>=0)
                {
                    g[j]=(f[j]-g[j-w[i]]+10)%10;
                }
                else
                {
                    g[j]=f[j]%10;
                }
                cout<<g[j];
            }
            cout<<endl;
        }
        return 0;
    }
    

\(B\) luogu P2371 [国家集训队] 墨墨的等式

\(C\) luogu P8392 [BalticOI 2022 Day1] Uplifting Excursion

  • 先将所有数贪心加入答案,然后进行调整。

  • 设此时重量为 \(w\) ,若 \(w>m\) 则从大往小进行删使在满足 \(w \le m\) 的情况下尽可能大,若 \(w \le m\) 则从小往大进行删使在满足 \(w \le m\) 的情况下尽可能大。这时一定有 \(w \in [m-n,m]\)

  • 此时背包剩余容量 \(\in [0,n]\) ,考虑进行背包 \(DP\) 调整。一方面先前选择的数可以撤销,另一方面可以添加新的数进去。故在加入顺序的调整过程中,始终有背包容量 \(\in [-n,n]\)

  • \(i\)\(-i\) 在新的背包转移过程中不会同时出现,即最多一共再放 \(2n\) 个数,转移用的背包容量开 \([-n^{2},n^{2}]\) 即可。

  • 需要二进制优化多重背包转移。

    点击查看代码
    ll a[610],b[610],f[200010];
    void dp(ll v,ll w,ll c,ll m)
    {
    	for(ll k=1;k<=c;k<<=1)
    	{
    		if(v>=0)
    		{
    			for(ll i=m;i>=k*v;i--)
    			{
    				f[i]=max(f[i],f[i-k*v]+k*w);
    			}
    		}
    		else
    		{
    			for(ll i=0;i-k*v<=m;i++)
    			{
    				f[i]=max(f[i],f[i-k*v]+k*w);
    			}
    		}
    		c-=k;
    	}
    	if(c>0)
    	{
    		if(v>=0)
    		{
    			for(ll i=m;i>=c*v;i--)
    			{
    				f[i]=max(f[i],f[i-c*v]+c*w);
    			}
    		}
    		else
    		{
    			for(ll i=0;i-c*v<=m;i++)
    			{
    				f[i]=max(f[i],f[i-c*v]+c*w);
    			}
    		}
    	}
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,m,sum=0,num[2]={0,0},i;
    	cin>>n>>m;
    	for(i=-n;i<=n;i++)
    	{	
    		cin>>a[i+n];
    		b[i+n]=a[i+n];
    		sum+=i*a[i+n];
    		num[i>=0]+=i*a[i+n];
    	}
    	if(num[1]<m||num[0]>m)
    	{
    		cout<<"impossible"<<endl;
    	}
    	else
    	{
    		if(sum>=m)
    		{
    			for(i=n;i>=1&&sum>m;i--)
    			{	
    				b[i+n]-=min(a[i+n],(sum-m)/i);
    				sum-=i*(a[i+n]-b[i+n]);
    			}
    		}
    		else
    		{
    			for(i=-n;i<=-1&&sum<m;i++)
    			{
    				b[i+n]-=min(a[i+n],(sum-m)/i);
    				sum-=i*(a[i+n]-b[i+n]);
    			}
    		}
    		memset(f,-0x3f,sizeof(f));
    		f[sum-m+n*n]=0;
    		for(i=-n;i<=n;i++)
    		{
    			f[sum-m+n*n]+=b[i+n];
    		}
    		for(i=-n;i<=n;i++)
    		{
    			if(i!=0)
    			{
    				dp(-i,-1,b[i+n],n*n*2);
    				dp(i,1,a[i+n]-b[i+n],n*n*2);
    			}
    		}
    		if(f[n*n]<0)
    		{
    			cout<<"impossible"<<endl;
    		}
    		else
    		{
    			cout<<f[n*n]<<endl;
    		}
    	}
    	return 0;
    }
    

\(D\) luogu P4322 [JSOI2016] 最佳团体

  • \(01\) 分数规划,设当前二分答案为 \(mid\) ,将点权拆成 \(p_{i}-mid \times s_{i}\) ,做树上背包后判断最大总和是否 \(\ge 0\)

    点击查看代码
    const double eps=1e-6;
    int s[2510],p[2510],siz[2510];
    double f[2510][2510],w[2510];
    vector<int>e[2510];
    void add(int u,int v)
    {
    	e[u].push_back(v);
    }
    void dfs(int x,int m)
    {
    	siz[x]=1;
    	f[x][1]=w[x];
    	for(int i=0;i<e[x].size();i++)
    	{
    		dfs(e[x][i],m);
    		for(int j=min(siz[x],m);j>=1;j--)
    		{
    			for(int k=siz[e[x][i]];k>=1;k--)
    			{
    				if(j+k<=m) f[x][j+k]=max(f[x][j+k],f[x][j]+f[e[x][i]][k]);
    			}
    		}
    		siz[x]+=siz[e[x][i]];
    	}
    }
    bool check(double mid,int n,int m)
    {
    	for(int i=1;i<=n;i++)
    	{
    		w[i]=p[i]-mid*s[i];	
    	}
    	for(int i=0;i<=n;i++)
    	{
    		fill(f[i]+1,f[i]+1+m,-0x3f3f3f3f);
    	}
    	dfs(0,m);
    	return f[0][m]>=0;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	int n,m,u,i;
    	double l=0,r=1e9,ans=0,mid;
    	cin>>m>>n;
    	m++;
    	for(i=1;i<=n;i++)
    	{
    		cin>>s[i]>>p[i]>>u;
    		add(u,i);
    	}
    	while(r-l>eps)
    	{
    		mid=(l+r)/2;
    		if(check(mid,n,m)==true)
    		{
    			l=mid;
    			ans=mid;
    		}
    		else
    		{
    			r=mid;
    		}
    	}
    	printf("%.3lf\n",ans);
    	return 0;
    }
    

\(E\) luogu P3592 [POI2015] MYJ

  • 观察到 \(n \le 50,m \le 4000\) ,先将 \(\{ c \}\) 进行离散化。

  • \(f_{l,r,i}\) 表示区间 \([l,r]\) 的最小值为 \(i\) 时,对 \([a,b]\) 完全包含于 \([i,j]\) 的人的最大收益,转移时考虑枚举最小值位置,状态转移方程为 \(f_{l,r,i}=\max\limits_{k \in [l,r],i \le p,q}^{r}\{ f_{l,k-1,p}+f_{k+1,r,q}+cost(l,r,k,i) \}\) ,特别地有 \(\forall i \in [1,n+1],f_{i,i-1}=0\)

  • \(\{ f \}\) 取后缀 \(\max\)\(O(nm)\) 暴力计算 \(cost(l,r,k,i)\) 即可。

    点击查看代码
    ll a[4010],b[4010],c[4010],d[4010],f[60][60][4010],cost[60][4010],ans[60];
    pair<ll,ll>opt[60][60][4010];
    void print(ll l,ll r,ll i)
    {
    	if(l>r)
    	{
    		return;
    	}
    	i=opt[l][r][i].second;//因内存访问不连续的常数问题,删去本行后在 LibreOJ 上无法通过
    	ans[opt[l][r][i].first]=d[opt[l][r][i].second];
    	print(l,opt[l][r][i].first-1,opt[l][r][i].second);
    	print(opt[l][r][i].first+1,r,opt[l][r][i].second);
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,m,i,len,l,r,k;
    	cin>>n>>m;
    	for(i=1;i<=m;i++)
    	{
    		cin>>a[i]>>b[i]>>c[i];
    		d[i]=c[i];
    	}
    	sort(d+1,d+1+m);
    	d[0]=unique(d+1,d+1+m)-(d+1);
    	for(i=1;i<=m;i++)
    	{
    		c[i]=lower_bound(d+1,d+1+d[0],c[i])-d;
    	}
    	for(len=1;len<=n;len++)
    	{
    		for(l=1,r=l+len-1;r<=n;l++,r++)
    		{
    			memset(cost,0,sizeof(cost));
    			for(i=1;i<=m;i++)
    			{
    				if(l<=a[i]&&b[i]<=r)
    				{
    					for(k=a[i];k<=b[i];k++)
    					{
    						cost[k][c[i]]++;
    					}
    				}
    			}
    			for(i=d[0];i>=1;i--)
    			{
    				opt[l][r][i]=make_pair(l,i);
    				for(k=l;k<=r;k++)
    				{
    					cost[k][i]+=cost[k][i+1];
    					if(f[l][k-1][i]+f[k+1][r][i]+cost[k][i]*d[i]>f[l][r][i])
    					{
    						f[l][r][i]=f[l][k-1][i]+f[k+1][r][i]+cost[k][i]*d[i];
    						opt[l][r][i].first=k;
    					}
    				}
    				if(f[l][r][i]<f[l][r][i+1])
    				{
    					f[l][r][i]=f[l][r][i+1];
    					opt[l][r][i]=opt[l][r][i+1];
    				}
    			}
    		}
    	}
    	cout<<f[1][n][1]<<endl;	
    	print(1,n,1);
    	for(i=1;i<=n;i++)
    	{
    		cout<<ans[i]<<" ";
    	}
    	return 0;
    }
    

\(F\) [AGC026D] Histogram Coloring

  • 手摸样例发现相邻两列同一段的方案数只与从最底层开始的极长蓝红交错连续段的长度有关(其他的部分必然是将前一列的颜色取反得到下一列的颜色)。

  • 相邻两列的拼接处要么完全相反,要么完全相同(且要求必须所有拼接的地方都是蓝红交错)。

  • \(f_{i,j}\) 表示处理到第 \(i\) 列时,极长蓝红交错连续段的长度为 \(j\) 的方案数。

  • \(h_{i}<h_{i-1}\) ,则有 \(\begin{cases} \forall j \in [1,h_{i}),f_{i,j}=f_{i-1,j} \\ f_{i,h_{i}}=\sum\limits_{j=h_{i}}^{h_{i-1}}2f_{i-1,j} \end{cases}\) 。若 \(h_{i} \ge h_{i-1}\) ,则有 \(\begin{cases} \forall j \in [1,h_{i}),f_{i,j}=2^{h_{i}-\max(j,h_{i-1})}f_{i-1,\min(j,h_{i-1})} \\ f_{i,h_{i}}=2f_{i-1,h_{i-1}} \end{cases}\) 。边界为 \(h_{0}=f_{0,1}=1\)

  • 观察到值域较大,离散化后分段计算总和即可,需要等比数列求和公式辅助计算。

    点击查看代码
    const ll p=1000000007;
    ll h[110],d[110],f[110];
    ll qpow(ll a,ll b,ll p)
    {
    	ll ans=1;
    	while(b)
    	{
    		if(b&1)
    		{
    			ans=ans*a%p;
    		}
    		b>>=1;
    		a=a*a%p;
    	}
    	return ans;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,ans=0,i,j;
    	cin>>n;
    	for(i=1;i<=n;i++)
    	{
    		cin>>h[i];
    		d[i]=h[i];
    	}
    	f[1]=h[0]=d[n+1]=1;
    	sort(d+1,d+1+n+1);
    	d[0]=unique(d+1,d+1+n+1)-(d+1);
    	for(i=1;i<=n;i++)
    	{		
    		h[i]=lower_bound(d+1,d+1+d[0],h[i])-d;
    	}	
    	for(i=1;i<=n;i++)
    	{
    		if(h[i]>=h[i-1])
    		{
    			for(j=1;j<=h[i-1]-1;j++)
    			{
    				f[j]=qpow(2,d[h[i]]-d[h[i-1]],p)*f[j]%p;
    			}
    			f[h[i]]=2*f[h[i-1]]%p;
    			for(j=h[i]-1;j>=h[i-1];j--)
    			{
    				f[j]=2*(qpow(2,d[h[i]]-d[j],p)-qpow(2,d[h[i]]-d[j]-(d[j+1]-d[j]),p)+p)%p*f[h[i-1]]%p;
    			}
    		}
    		else
    		{
    			f[h[i]]=2*f[h[i]]%p;
    			for(j=h[i]+1;j<=h[i-1];j++)
    			{
    				f[h[i]]=(f[h[i]]+2*f[j])%p;
    			}
    		}
    	}
    	for(i=1;i<=h[n];i++)
    	{
    		ans=(ans+f[i])%p;
    	}
    	cout<<ans<<endl;
    	return 0;
    }
    
    

\(G\) CF1372E Omkar and Last Floor

  • \(a^{2}+b^{2} \ge (a+b)^{2}\) ,考虑尽可能让某一列的 \(1\) 的个数最多。

  • \(f_{l,r}\) 表示区间 \([l,r]\) 内的最大权值,转移时仍考虑枚举最大值位置,状态转移方程为 \(f_{l,r}=\max\limits_{k \in [l,r]} \{ f_{l,k-1}+f_{k+1,r}+cost(l,r,k) \}\)

  • \(cost(l,r,k)\) 可以在转移时暴力计算,也可以二维偏序预处理,后者写法类似 NOIP2024模拟2 T3 GHzoj 3733. 线段树

    点击查看代码
    int sum[110],f[110][110],cost[110][110][110];
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	int n,m,i,j,k,l,r,len;
    	cin>>n>>m;
    	for(i=1;i<=n;i++)
    	{
    		cin>>sum[i];
    		for(j=1;j<=sum[i];j++)
    		{
    			cin>>l>>r;
    			for(k=l;k<=r;k++)
    			{
    				cost[l][r][k]++;
    			}
    		}
    	}
    	for(len=1;len<=m;len++)
    	{
    		for(l=1,r=l+len-1;r<=m;l++,r++)
    		{
    			for(k=l;k<=r;k++)
    			{
    				cost[l][r][k]+=cost[l+1][r][k]+cost[l][r-1][k]-cost[l+1][r-1][k];
    				f[l][r]=max(f[l][r],f[l][k-1]+f[k+1][r]+cost[l][r][k]*cost[l][r][k]);
    			}
    		}
    	}
    	cout<<f[1][m]<<endl;
    	return 0;
    }
    

\(H\) luogu P6563 [SBCOI2020] 一直在你身旁

  • \(f_{l,r}\) 表示在确定电线长度 \(\in [l,r]\) ,至少要花多少钱才能保证知道电线的长度,状态转移方程为 \(f_{l,r}=\min\limits_{k=l}^{r-1}\{ \max(f_{l,k},f_{k+1,r})+a_{k} \}\)

  • 此时单次询问时间复杂度为 \(O(n^{3})\) ,无法接受,考虑进一步优化。

  • 显然 \(f\) 具有区间包含单调性,但因前面是 \(\max(f_{l,k},f_{k+1,r})\) 不保证存在决策单调性。

  • 观察到 \(f_{l,k},f_{k+1,r}\) 的临界点具有单调性,以临界点拆成两部分后单调队列优化转移即可。

  • 略带卡常。

    点击查看代码
    ll a[7110],f[7110][7110];
    deque<ll>q;
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin); 
    	freopen("out.out","w",stdout);
    #endif
    	ll t,n,i,j,l,r,opt;
    	scanf("%lld",&t);
    	for(j=1;j<=t;j++)
    	{
    		scanf("%lld",&n);
    		for(i=1;i<=n;i++)
    		{
    			scanf("%lld",&a[i]);
    			fill(f[i]+1,f[i]+1+n,0);
    		}
    		for(r=1;r<=n;r++)
    		{
    			q.clear();
    			for(l=opt=r-1;l>=1;l--)
    			{
    				while(opt>=l&&f[opt][l]>=f[r][opt+1])
    				{
    					opt--;
    				}
    				f[r][l]=f[opt+1][l]+a[opt+1];
    				while(q.empty()==0&&q.back()>=opt+1)
    				{
    					q.pop_back();
    				}
    				if(q.empty()==0)
    				{
    					f[r][l]=min(f[r][l],f[r][q.back()+1]+a[q.back()]);
    				}
    				while(q.empty()==0&&f[r][q.front()+1]+a[q.front()]>f[r][l+1]+a[l])
    				{
    					q.pop_front();
    				}
    				q.push_front(l);
    			}
    		}
    		printf("%lld\n",f[n][1]);
    	}
    	return 0;
    }
    

\(I\) CF868F Yet Another Minimization Problem

  • 猜测代价函数 \(w(l,r)\) 具有决策单调性,做 \(k\) 遍分治即可。

  • 另外一个问题是如何较快地得到 \(w(l,r)\) ,不妨类似莫队一样维护左右指针,因决策范围逐渐缩小故指针暴力移动的复杂度是正确的。

    点击查看代码
    ll a[100010],cnt[100010],f[30][100010],l=1,r=0,sum=0;
    void add(ll x)
    {
    	sum+=cnt[a[x]];
    	cnt[a[x]]++;
    }
    void del(ll x)
    {
    	cnt[a[x]]--;
    	sum-=cnt[a[x]];
    }
    ll w(ll x,ll y)
    {
    	while(l>x)
    	{
    		l--;  add(l);
    	}
    	while(r<y)
    	{
    		r++;  add(r);
    	}
    	while(l<x)
    	{
    		del(l);  l++;
    	}
    	while(r>y)
    	{
    		del(r);  r--;
    	}
    	return sum;
    }
    void solve(ll l,ll r,ll x,ll y,ll dep)
    {
    	if(l>r)  return;
    	ll mid=(l+r)/2,opt=0;
    	for(ll i=max(x,1ll);i<=min(y,mid);i++)// dep>=2 且 mid=1 时无定义值
    	{
    		if(f[dep-1][i-1]+w(i,mid)<f[dep][mid])
    		{
    			f[dep][mid]=f[dep-1][i-1]+w(i,mid);
    			opt=i;
    		}
    	}
    	solve(l,mid-1,x,opt,dep);  solve(mid+1,r,opt,y,dep);
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,m,i;
    	cin>>n>>m;  memset(f,0x3f,sizeof(f));
    	for(i=1;i<=n;i++)  cin>>a[i];
    	f[0][0]=0;
    	for(i=1;i<=m;i++)  solve(1,n,1,n,i);
    	cout<<f[m][n]<<endl;
    	return 0;
    }
    

\(J\) LibreOJ 6039. 「雅礼集训 2017 Day5」珠宝 /「NAIPC2016」Jewel Thief

\(K\) luogu P3648 [APIO2014] 序列分割

\(L\) CF1175G Yet Another Partiton Problem

  • \(f_{i,j}\) 表示把前 \(i\) 个数划分成 \(j\) 段的最小划分权值,状态转移方程为 \(f_{i,j}=\min\limits_{k=j-1}^{i-1}\{ f_{k,j-1}+(i-k)\max\limits_{h=k+1}^{i}\{ a_{h} \} \}\)

  • \(v=\max\limits_{h=k+1}^{i}\{ a_{h} \}\) ,观察当右端点 \(i\) 固定时,左边会依据 \(v\) 分割成若干段,考虑单调栈维护这个过程。

  • 在每一段的内部需要知道 \(f_{k,j-1}-kv\) 的最小值作为截距插入李超线段树内,考虑在凸包上二分。这样的话,合并单调栈的过程中同时需要合并两个凸包和删除直线,考虑分别使用启发式合并和可持久化李超线段树维护。

  • 查询时只需要查询 \(x=i\) 处的最小值即可。

    点击查看代码
    struct line
    {
    	int k,b;
    }li[20010];
    int a[20010],dp[20010],last[20010];
    deque<int>q[20010];
    stack<int>s;
    int f(int id,int x)
    {
    	return li[id].k*x+li[id].b;
    }
    int cmp(int a,int b,int x)
    {
    	if(f(a,x)-f(b,x)<0)  return true;
    	if(f(b,x)-f(a,x)<0)  return false;
    	return a<b;
    }
    struct LiChao_Tree	
    {
    	int root[20010],rt_sum;
    	struct SegmentTree
    	{
    		int ls,rs,id;
    	}tree[20010<<5];
    	#define lson(rt) (tree[rt].ls)
    	#define rson(rt) (tree[rt].rs)
    	int build_rt()
    	{
    		rt_sum++;
    		lson(rt_sum)=rson(rt_sum)=tree[rt_sum].id=0;
    		return rt_sum;
    	}
    	void clear()
    	{
    		rt_sum=0;
    		memset(root,0,sizeof(root));
    	}
    	void add(int pre,int &rt,int l,int r,int id)
    	{
    		rt=build_rt();
    		tree[rt]=tree[pre];
    		if(pre==0)
    		{
    			tree[rt].id=id;
    			return;
    		}
    		int mid=(l+r)/2;
    		if(cmp(tree[rt].id,id,mid)==false)  swap(tree[rt].id,id);
    		if(l==r)  return;
    		if(cmp(tree[rt].id,id,l)==false)  add(lson(pre),lson(rt),l,mid,id);
    		else  lson(rt)=lson(pre);
    		if(cmp(tree[rt].id,id,r)==false)  add(rson(pre),rson(rt),mid+1,r,id);
    		else  rson(rt)=rson(pre);
    	}
    	int query(int rt,int l,int r,int pos)
    	{
    		if(rt==0) 
    		{
    			return 0x3f3f3f3f;
    		}
    		if(l==r)
    		{
    			return f(tree[rt].id,pos);
    		}
    		int mid=(l+r)/2;
    		if(pos<=mid)
    		{
    			return min(f(tree[rt].id,pos),query(lson(rt),l,mid,pos));
    		}
    		else
    		{
    			return min(f(tree[rt].id,pos),query(rson(rt),mid+1,r,pos));
    		}
    	}
    }T;
    int x(int i)
    {
    	return i;
    }
    int y(int i)
    {
    	return last[i];
    }
    void merge(int i,int j)
    {
    	if(q[i].size()<q[j].size())
    	{
    		while(q[i].empty()==0)
    		{
    			while(q[j].size()>=2&&1ll*(y(q[j][1])-y(q[j].front()))*(x(q[i].back())-x(q[j].front()))>=1ll*(y(q[i].back())-y(q[j].front()))*(x(q[j][1])-x(q[j].front())))
    			{
    				q[j].pop_front();
    			}
    			q[j].push_front(q[i].back());
    			q[i].pop_back();
    		}
    	}
    	else
    	{
    		while(q[j].empty()==0)
    		{
    			while(q[i].size()>=2&&1ll*(y(q[i].back())-y(q[i][q[i].size()-2]))*(x(q[j].front())-x(q[i].back()))>=1ll*(y(q[j].front())-y(q[i].back()))*(x(q[i].back())-x(q[i][q[i].size()-2])))
    			{
    				q[i].pop_back();
    			}
    			q[i].push_back(q[j].front());
    			q[j].pop_front();
    		}
    		q[j].swap(q[i]);
    	}
    }
    bool check(int mid,int id,int k)
    {
    	return 1ll*(y(q[id][mid+1])-y(q[id][mid]))<=1ll*k*(x(q[id][mid+1])-x(q[id][mid]));
    }
    int divide(int id,int k)
    {
    	int l=0,r=q[id].size()-1,mid,ans=0;
    	while(l<r)
    	{
    		mid=(l+r)/2;
    		if(check(mid,id,k)==true)
    		{
    			ans=mid+1;
    			l=mid+1;
    		}
    		else
    		{
    			r=mid;
    		}
    	}
    	return last[q[id][ans]]-q[id][ans]*k;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	int n,m,i,j;
    	cin>>n>>m;
    	for(i=1;i<=n;i++)
    	{
    		cin>>a[i];
    	}
    	memset(dp,0x3f,sizeof(dp));
    	dp[0]=0;
    	for(j=1;j<=m;j++)
    	{
    		swap(last,dp);
    		while(s.empty()==0)
    		{
    			s.pop();
    		}
    		T.clear();
    		for(i=1;i<=n;i++)
    		{
    			q[i].clear();
    			q[i].push_back(i-1);
    			while(s.empty()==0&&a[s.top()]<=a[i])
    			{
    				merge(s.top(),i);
    				s.pop();
    			}
    			li[i].k=a[i];
    			li[i].b=divide(i,a[i]);
    			T.add(T.root[(s.empty()==0)?s.top():0],T.root[i],1,n,i);
    			dp[i]=T.query(T.root[i],1,n,i);
    			s.push(i);
    		}
    	}
    	cout<<dp[n]<<endl;
    	return 0;
    }
    

\(M\) luogu P4655 [CEOI2017] Building Bridges

\(N\) luogu P4383 [八省联考 2018] 林克卡特树

  • 断开 \(k\) 条边后形成了 \(k+1\) 个连通块,最大化每个连通块直径之和后再用新加的 \(0\) 边权的边使其相连。

  • \(f_{x,i,0/1/2}\) 表示以 \(x\) 为根的子树内选出了 \(i\) 条互不相交的链,且 \(x\) 不在任意一条链上/在一条链上作为端点/在一条链上作为中间的点的链长之和。

  • \(DFS\) 过程中枚举相邻的边转移是容易的,但貌似没有什么可优化的地方。

  • 猜测在 \(i\) 这一维上满足凸性,使用 WQS 二分维护。

    点击查看代码
    const ll inf=1000000000000;
    struct node
    {
    	ll nxt,to,w;
    }e[600010];
    struct quality
    {
    	ll mx,cnt;
    	quality operator + (const quality &another) const
    	{
    		return (quality){mx+another.mx,cnt+another.cnt};
    	}
    	bool operator < (const quality &another) const
    	{
    		return (mx==another.mx)?(cnt<another.cnt):(mx<another.mx);
    	}
    }f[3][300010],g;
    ll head[300010],cnt=0;
    void add(ll u,ll v,ll w)
    {
    	cnt++;  e[cnt]=(node){head[u],v,w};  head[u]=cnt;
    }
    void dfs(ll x,ll fa,ll mid)
    {
    	f[0][x]=(quality){0,0};  f[1][x]=(quality){-inf,inf};  f[2][x]=(quality){-mid,1};
    	for(ll i=head[x];i!=0;i=e[i].nxt)
    	{
    		ll y=e[i].to;
    		if(y==fa)  continue;
    		dfs(y,x,mid);  g=max({f[0][y],f[1][y],f[2][y]});
    		f[2][x]=max(f[2][x]+g,f[1][x]+max(
    			f[0][y]+(quality){e[i].w,0},f[1][y]+(quality){e[i].w+mid,-1}));
    		f[1][x]=max(f[1][x]+g,f[0][x]+max(
    			f[0][y]+(quality){e[i].w-mid,1},f[1][y]+(quality){e[i].w,0}));
    		f[0][x]=f[0][x]+g;
    	}
    }
    bool check(ll mid,ll m)
    {
    	dfs(1,0,mid);
    	return max({f[0][1],f[1][1],f[2][1]}).cnt>=m;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,m,u,v,w,l=-inf,r=inf,ans=0,mid,i;
    	cin>>n>>m;  m++;
    	for(i=1;i<=n-1;i++)
    	{
    		cin>>u>>v>>w;
    		add(u,v,w);  add(v,u,w);
    	}
    	while(l<=r)
    	{
    		mid=(l+r)>>1;
    		if(check(mid,m)==true)
    		{
    			ans=mid;
    			l=mid+1;
    		}
    		else  r=mid-1;
    	}
    	check(ans,m);
    	cout<<max({f[0][1],f[1][1],f[2][1]}).mx+ans*m<<endl;
    	return 0;
    }
    

\(O\) CF321E Ciel and Gondolas

  • 代价函数满足四边形不等式是显然的。

  • 二分队列辅助 WQS 二分。

    点击查看代码
    namespace IO{
    	#ifdef LOCAL
    	FILE*Fin(fopen("test.in","r")),*Fout(fopen("test.out","w"));
    	#else
    	FILE*Fin(stdin),*Fout(stdout);
    	#endif
    	class qistream{static const size_t SIZE=1<<16,BLOCK=64;FILE*fp;char buf[SIZE];int p;public:qistream(FILE*_fp=stdin):fp(_fp),p(0){fread(buf+p,1,SIZE-p,fp);}void flush(){memmove(buf,buf+p,SIZE-p),fread(buf+SIZE-p,1,p,fp),p=0;}qistream&operator>>(char&x){x=getch();while(isspace(x))x=getch();return*this;}template<class T>qistream&operator>>(T&x){x=0;p+BLOCK>=SIZE?flush():void();bool flag=false;for(;!isdigit(buf[p]);++p)flag=buf[p]=='-';for(;isdigit(buf[p]);++p)x=x*10+buf[p]-'0';x=flag?-x:x;return*this;}char getch(){p+BLOCK>=SIZE?flush():void();return buf[p++];}qistream&operator>>(char*str){char ch=getch();while(ch<=' ')ch=getch();int i=0;for(;ch>' ';++i,ch=getch())str[i]=ch;str[i]='\0';return*this;}}qcin(Fin);
    	class qostream{static const size_t SIZE=1<<16,BLOCK=64;FILE*fp;char buf[SIZE];int p;public:qostream(FILE*_fp=stdout):fp(_fp),p(0){}~qostream(){fwrite(buf,1,p,fp);}void flush(){fwrite(buf,1,p,fp),p=0;}template<class T>qostream&operator<<(T x){int len=0;p+BLOCK>=SIZE?flush():void();x<0?(x=-x,buf[p++]='-'):0;do buf[p+len]=x%10+'0',x/=10,++len;while(x);for(int i=0,j=len-1;i<j;++i,--j)std::swap(buf[p+i],buf[p+j]);p+=len;return*this;}qostream&operator<<(char x){putch(x);return*this;}void putch(char ch){p+BLOCK>=SIZE?flush():void();buf[p++]=ch;}qostream&operator<<(char*str){for(int i=0;str[i];++i)putch(str[i]);return*this;}qostream&operator<<(const char*s){for(int i=0;s[i];++i)putch(s[i]);return*this;}}qcout(Fout);
    }
    #define cin IO::qcin
    #define cout IO::qcout
    const ll inf=100000000000000;
    struct node
    {
    	ll opt,l,r;
    };
    ll a[4010][4010],sum[4010][4010],f[4010],g[4010];
    deque<node>q;
    ll w(ll l,ll r)
    {
    	l++;
    	return f[l-1]+sum[r][r]-sum[r][l-1]-sum[l-1][r]+sum[l-1][l-1];
    }
    ll divide(ll n,ll i)
    {
    	ll l=q.back().l,r=n,mid;
    	while(l<=r)
    	{
    		mid=(l+r)/2;
    		if(w(q.back().opt,mid)>=w(i,mid))  r=mid-1;
    		else  l=mid+1;
    	}
    	q.back().r=r;
    	return l;
    }
    bool check(ll n,ll m,ll mid)
    {
    	q.clear();  q.push_back((node){0,1,n});
    	for(ll i=1;i<=n;i++)
    	{
    		while(q.empty()==0&&q.front().r<i)  q.pop_front();
    		if(q.empty()==0)
    		{
    			q.front().l=i;
    			f[i]=w(q.front().opt,i)-mid;
    			g[i]=g[q.front().opt]+1;
    		}
    		while(q.empty()==0&&w(q.back().opt,q.back().l)>=w(i,q.back().l))  q.pop_back();
    		if(q.empty()==0)
    		{
    			if(w(q.back().opt,n)>w(i,n))  q.push_back((node){i,divide(n,i),n});
    		}
    		else  q.push_back((node){i,i,n});
    	}
    	return g[n]>=m;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,m,l=-inf,r=inf,mid,ans=0,i,j;
    	cin>>n>>m;
    	for(i=1;i<=n;i++)
    	{
    		for(j=1;j<=n;j++)
    		{
    			cin>>a[i][j];
    			sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];
    		}
    	}
    	while(l<=r)
    	{
    		mid=(l+r)>>1;
    		if(check(n,m,mid)==true)
    		{
    			ans=mid;
    			r=mid-1;
    		}
    		else  l=mid+1;
    	}
    	check(n,m,ans);
    	cout<<(f[n]+ans*m)/2<<endl;
    	return 0;
    }
    

\(P\) [ABC305Ex] Shojin

  • [ABC366F] Maximum Composition 的结论,每个子段内部按照 \(\frac{b}{a-1}\) 升序排序。同时每一段的长度至多为 \(O(\log X)\)

    • 实际代码实现中去掉了 \(a_{i}=1\) 的部分。
  • 猜测代价函数满足四边形不等式,同时 WQS 二分并不仅仅局限于解决有段数限制的题目,故可以据此根据切到的点判断接下来的方向。

  • 但由于共线问题,导致找到的 \(k\) 可能并不是最终的 \(k\) ,需要进一步找到端点处的答案。建议辅助代码理解。

    点击查看代码
    const ll inf=1000000000000,limit=28;
    ll a[200010],b[200010],p[200010],w[30][200010],f[200010],g[200010];
    bool cmp(ll x,ll y)
    {
    	return b[x]*(a[y]-1)<b[y]*(a[x]-1);
    }
    bool check(ll n,ll m,ll mid)
    {
    	for(ll i=1;i<=n;i++)
    	{
    		f[i]=inf;  g[i]=0;
    		for(ll len=1,j=i;len<=limit&&j>=1;len++,j--)
    		{
    			if(f[j-1]+w[len][j]+mid<f[i]) 
    			{
    				f[i]=f[j-1]+w[len][j]+mid;
    				g[i]=g[j-1]+1;
    			}
    			else  if(f[j-1]+w[len][j]+mid==f[i])  g[i]=max(g[i],g[j-1]+1);
    		}
    	}
    	return f[n]-g[n]*mid<=m;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,_n=0,m,sum=0,x,y,len,l,r,mid,ans=0,i;
    	cin>>n>>m;
    	for(i=1;i<=n;i++)
    	{
    		cin>>x>>y;
    		if(x==1)  sum+=y;
    		else
    		{
    			_n++;
    			a[_n]=x;  b[_n]=y;
    		}
    	}
    	if(_n==0)  cout<<1<<" "<<sum<<endl;
    	else
    	{
    		n=_n;  m-=sum;
    		for(len=1;len<=limit;len++)
    		{
    			for(l=1,r=l+len-1;r<=n;l++,r++)
    			{
    				for(i=1;i<=len;i++)  p[i]=l+i-1;
    				sort(p+1,p+1+len,cmp);
    				for(i=1;i<=len;i++)  w[len][l]=min(inf,w[len][l]*a[p[i]]+b[p[i]]);
    			}
    		}
    		l=-inf;  r=inf;
    		while(l<=r)
    		{
    			mid=(l+r)>>1;
    			if(check(n,m,mid)==true)
    			{
    				ans=mid;
    				l=mid+1;
    			}
    			else  r=mid-1;
    		}
    		check(n,m,ans);  g[n]-=(m-(f[n]-g[n]*ans))/ans;
    		cout<<g[n]<<" "<<sum+f[n]-g[n]*ans<<endl;
    	}
    	return 0;
    }
    

\(Q\) CF713C Sonya and Problem Wihtout a Legend

  • 多倍经验: CF13C Sequence

  • Slope Trick 板子。

    • Slope Trick 常用于维护含有连续的分段一次凸函数 \(f(x)\) ,且每一段的斜率较小并均为整数,例如绝对值函数 \(f(x)=|x|\)
      • 不难发现,同时满足上面性质的两个函数 \(f(x),g(x)\) 相加后的函数 \(h(x)=f(x)+g(x)\) 仍满足上面的性质。
    • 考虑维护 \(x_{0}\) 时的函数值 \(f(x_{0})\) 和斜率 \(k_{0}\) ,以及每个斜率变化点的可重集合 \(S\) 。具体地,若函数 \(f(x)\)\(x\) 处斜率增加了 \(\Delta k\) 则往 \(S\) 中加入 \(|\Delta k|\)\(x\) ,例如对于 \(f(x)=|x|\)\(S_{f}=\{ 0,0 \}\)
    • Slope Trick 支持满足上述性质的函数间进行一系列操作。
      • 函数 \(f(x),g(x)\) 相加时,有 \(\begin{cases} h(x_{0})=f(x_{0})+g(x_{0}) \\ k_{h(x_{0})}=k_{f(x_{0})}+k_{g(x_{0})} \\ S_{h}=S_{f} \bigcup S_{g} \end{cases}\)
      • 对函数 \(f(x)\) 取前、后缀 \(\min / \max\) :根据函数上凸性或下凸性,去掉 \(k<0\)\(k>0\) 的部分。
      • \(\min \{ f(x) \} / \max \{ f(x) \}\) 及取到相应值的 \(x\) :取 \(k=0\) 的部分。
      • 平移某段函数值:维护 \(f(x_{0}),k_{0}\) 的变化并在斜率变化点打上平移标记。
      • 翻转某段函数值:维护 \(f(x_{0}),k_{0}\) 的变化并在斜率变化点打上翻转标记。
  • 先令 \(a_{i} \gets a_{i}-i\) ,将严格递增转化为单调不降。

  • \(f_{i,j}\) 表示处理到 \(i\)\(a_{i}=j\) 的最小操作次数,状态转移方程为 \(f_{i,j}=\min\limits_{k \le j}\{ f_{i-1,k} \}+|a_{i}-j|\) 。设 \(F_{i}(x)=f_{i,x}\) ,此时 \(F_{i}\) 是由 \(F_{i-1}\) 取前缀 \(\min\) 后加一个绝对值函数得到的,删去 \(k>0\) 的部分后后半部分是一条水平直线,考虑维护变化点。

  • \(F_{i-1}\)\(h\) 时第一次取到最小值。

  • \(h \le a_{i}\) 时加入 \(a_{i}\)\((-\infty,a_{i}]\) 斜率减一,且在 \(a_{i}\) 处两侧斜率由 \(-1\) 变为 \(0\) ,故只加入一个 \(a_{i}\) 。否则需要将函数向上抬升 \(h-a_{i}\) 并加入最终答案,且 \(a_{i}\) 处两侧斜率变化了 \(1-(-1)=2\) ,需要加入两个 \(a_{i}\) ,同时最小值位置变成了 \(a_{i}\) ,需要删除 \(h\)

  • 优先队列维护即可。

    点击查看代码
    priority_queue<ll>q;
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,x,ans=0,i;
    	cin>>n;
    	for(i=1;i<=n;i++)
    	{
    		cin>>x;
    		if(q.empty()==0&&q.top()>x)
    		{
    			ans+=q.top()-x;
    			q.pop();
    			q.push(x);
    		}
    		q.push(x);	
    	}
    	cout<<ans<<endl;
    	return 0;
    }
    

\(R\) [ABC217H] Snuketoon

  • \(f_{i,j}\) 表示经历 \(i\) 次攻击后,当前位置在 \(j\) 点的最小总伤害值,状态转移方程为 \(\begin{cases} f_{i,j}=\min\limits_{k=j-(t_{i}-t_{i-1})}^{j+(t_{i}-t_{i-1})} \{ f_{i-1,k} \}+\max(0,x_{i}-j) & d_{i}=0 \\ f_{i,j}=\min\limits_{k=j-(t_{i}-t_{i-1})}^{j+(t_{i}-t_{i-1})} \{ f_{i-1,k} \}+\max(0,j-x_{i}) & d_{i}=1 \end{cases}\)

  • 区间取 \(\min\) 实际是将最小值左/右边的点分别向左/右平移,平移可以通过打标记维护,但只维护 \(k<0\)\(k>0\) 的部分就无法支持优化了。

  • 分别开两个优先队列记录左右两侧的斜率变化点(特别地,每个优先队列的顶部都是斜率为 \(0\) 的一个端点),分讨插入位置和斜率为 \(0\) 的区间包含关系对答案与区间更新即可。

  • 为保证插入点在下次更新时处于合法状态,可以一开始先各插入 \(n\)\(0\) 保证不合法的插入点不会被统计。

    点击查看代码
    ll t[200010],d[200010],x[200010];
    priority_queue<ll,vector<ll>,less<ll> >l;
    priority_queue<ll,vector<ll>,greater<ll> >r;
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,ans=0,lazyl=0,lazyr=0,i;
    	cin>>n;
    	for(i=1;i<=n;i++)
    	{
    		l.push(0);
    		r.push(0);
    	}
    	for(i=1;i<=n;i++)
    	{
    		cin>>t[i]>>d[i]>>x[i];
    		lazyl-=t[i]-t[i-1];
    		lazyr+=t[i]-t[i-1];
    		if(d[i]==0)
    		{
    			if(x[i]>r.top()+lazyr)
    			{
    				ans+=x[i]-(r.top()+lazyr);
    				l.push(r.top()+lazyr-lazyl);
    				r.pop();
    				r.push(x[i]-lazyr);
    			}
    			else
    			{
    				l.push(x[i]-lazyl);
    			}
    		}
    		else
    		{
    			if(x[i]<l.top()+lazyl)
    			{
    				ans+=(l.top()+lazyl)-x[i];
    				r.push(l.top()+lazyl-lazyr);
    				l.pop();
    				l.push(x[i]-lazyl);
    			}
    			else
    			{
    				r.push(x[i]-lazyr);
    			}
    		}
    	}
    	cout<<ans<<endl;
    	return 0;
    } 
    

\(S\) luogu P3642 [APIO2016] 烟火表演

  • \(f_{x,i}\) 表示将以 \(x\) 为根的子树内引爆时间均为 \(i\) 时的最小代价,状态转移方程为 \(f_{x,i}=\sum\limits_{y \in Son(x)}\min\limits_{j=0}^{i}\{ f_{y,j}+|z-(i-j)| \}\)

  • 每个儿子内部相互独立,可以单独计算。

  • 考虑进行 Slope Trick 优化。设 \(F_{x}(i)=f_{x,i},G_{x}(i)=\min\limits_{j=0}^{i}\{ f_{x,j}+|z-(i-j)| \}\) ,其中 \(F_{x}\) 是一个下凸包。

  • \(F_{x}\) 的斜率为 \(0\) 的部分为 \([l,r]\) ,此时问题来到了 \([0,l),[l,l+z),[l+z,r+z],(r+z,\infty)\) 处分别的取值。

    • 因为 \([0,l)\) 内单调递减,且将 \(z\) 修改成 \(0\) 一定比将子树内修改其他若干条边优,对 \(G_{x,i}\) 的贡献在 \(i\) 处取到最小值 \(F_{y}(i)+z\)
    • \([l,l+z)\) 内不一定必须将 \(z\) 修改成 \(0\) ,对 \(G_{x,i}\) 的贡献在 \(l\) 处取到最小值 \(F_{y}(l)+z-(i-l)\)
    • \([l+z,r+z]\) 内可以直接继承 \(i-z\) 的答案,对 \(G_{x,i}\) 的贡献在 \(i\) 处取到最小值 \(F_{y}(i)=F_{y}(l)\)
    • 因为 \((r+z,\infty)\) 内单调递增,且将 \(z\) 修改成 \(r+z\) 一定更优,对 \(G_{x,i}\) 的贡献在 \(i\) 处取到最小值 \(F_{y}(i)+i-(r+z)\)
  • 对函数每段单独的处理是容易实现的。考虑如何合并信息。

    • 仍考虑维护 \(G_{x}\) 的斜率变化点,合并时删去 \([l,\infty)\) 以内的变化点并加入 \(l+z,r+z\) 两个新点,剩下的变化点可以直接合并。可并堆维护即可。
    • 因每个儿子都对应加入一个新点,顺次弹出 \(F_{y}\)\(|Son(y)|-1\) 个变化点后即可得到 \(l,r\)
    • 统计答案时用 \(f_{1}(0)\) 处的值 \(\sum\limits_{(x,y,z) \in E}z\) 减去斜率 \(<0\) 部分的贡献。
    点击查看代码
    vector<pair<ll,ll> >e[300010];
    void add(ll u,ll v,ll w)
    {
    	e[u].push_back(make_pair(v,w));
    }   
    struct Heap
    {
    	ll root[600010],rt_sum;
    	struct Leftist_Tree
    	{
    		ll ls,rs,val,d;
    	}tree[600010];
    	#define lson(rt) (tree[rt].ls)
    	#define rson(rt) (tree[rt].rs)
    	ll build_rt(ll val)
    	{
    		rt_sum++;
    		lson(rt_sum)=rson(rt_sum)=0;
    		tree[rt_sum].val=val;
    		tree[rt_sum].d=1;
    		return rt_sum;
    	}
    	ll merge(ll rt1,ll rt2)
    	{
    		if(rt1==0||rt2==0)  return rt1+rt2;
    		if(tree[rt1].val<tree[rt2].val)  swap(rt1,rt2);
    		rson(rt1)=merge(rson(rt1),rt2);
    		if(tree[lson(rt1)].d<tree[rson(rt1)].d)  swap(lson(rt1),rson(rt1));
    		tree[rt1].d=tree[rson(rt1)].d+1;
    		return rt1;
    	}
    	ll query(ll rt)
    	{
    		return tree[rt].val;
    	}
    	void pop(ll &rt)
    	{
    		rt=merge(lson(rt),rson(rt));
    	}
    }T;
    void dfs(ll x)
    {
    	for(ll i=0;i<e[x].size();i++)
    	{
    		ll y=e[x][i].first,w=e[x][i].second;
    		dfs(y);
    		for(ll j=1;j<e[y].size();j++)  T.pop(T.root[y]);
    		ll r=T.query(T.root[y]);  T.pop(T.root[y]);
    		ll l=T.query(T.root[y]);  T.pop(T.root[y]);
    		T.root[y]=T.merge(T.root[y],T.merge(T.build_rt(l+w),T.build_rt(r+w)));
    		T.root[x]=T.merge(T.root[x],T.root[y]);
    	}
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll n,m,u,w,ans=0,i;
    	cin>>n>>m;
    	for(i=2;i<=n+m;i++)
    	{
    		cin>>u>>w;
    		ans+=w;
    		add(u,i,w);
    	}
    	dfs(1);
    	for(i=1;i<=e[1].size();i++)  T.pop(T.root[1]);
    	while(T.root[1]!=0)
    	{
    		ans-=T.query(T.root[1]);
    		T.pop(T.root[1]);
    	}
    	cout<<ans<<endl;
    	return 0;
    }
    

\(T\) luogu P1912 [NOI2009] 诗人小G

posted @ 2024-12-25 19:45  hzoi_Shadow  阅读(288)  评论(1)    收藏  举报
扩大
缩小