省选集训 26 - DP 优化

[ABC214G] Three Permutations

正难则反,令 \(f_i\) 表示钦定 \(i\) 个条件不满足的方案数,则 \(ans=\sum_{i=0}^n(-1)^if_i(n-i)!\)

\(p_i\)\(q_i\) 连双向边,则问题转化为将每条边标号为两端点标号之一且不重复。

由于每个点的度数均为 \(2\),所以图一定构成了若干个环。

记已处理完的答案为 \(f\),已处理完的大小为 \(s\),当前处理环的大小为 \(c\),新的 \(f\)\(g\)

\(c=1\) 时显然有 \(g_i=f_{i-1}+f_i\)

\(c\neq 1\) 时枚举 \(c\) 中选 \(i\) 条边,\(s\) 中选 \(j\) 条边,有 \(g_{i+j}\leftarrow h_{c,i}\times f_j\)

\(h_{c,i}\) 表示大小为 \(c\) 的环选 \(i\) 条边的方案数,考虑如何计算。

\((u,v)\) 边拆成 \((u,w)\)\((w,v)\),则选 \((u,w)\) 表示标 \(u\),选 \((w,v)\) 表示标 \(v\)

问题转化为在大小为 \(2c\) 的环上选 \(i\) 条不相邻边的方案数,考虑钦定其中一条边是否选。

\(h_{c,i}=w_{2c-1,i}+w_{2c-3,i-1}\)\(w_{i,j}\) 表示 \(i\) 条边的链选 \(j\) 条不相邻边的方案数。

根据组合意义有 \(w_{i,j}=w_{i-1,j}+w_{i-2,j-1}\),初值 \(w_{1,0}=w_{2,0}=1\)\(w_{2,1}=2\)

然后 DP 解决即可,复杂度证明:任意两点间贡献只会被统计一次,与树形 DP 类似。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define add(x,y) (x)=((x)+(y))%mod
const int N=6005,mod=1000000007;
vector<int> v[N];bool vis[N];
int n,num,now,ans,p[N],q[N],f[N],g[N],fac[N],w[N][N];
void dfs(int x){
	vis[x]=1,num++;
	for(auto y:v[x])  if(!vis[y])  dfs(y);
}
signed main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,fac[0]=f[0]=w[1][1]=1,w[2][1]=2;
	for(int i=1;i<N;i++)  w[i][0]=1,fac[i]=fac[i-1]*i%mod;
	for(int i=3;i<N;i++)
		for(int j=1;j<=i;j++)
			w[i][j]=(w[i-1][j]+w[i-2][j-1])%mod;
	for(int i=1;i<=n;i++)  cin>>p[i];
	for(int i=1;i<=n;i++)  cin>>q[i];
	for(int i=1;i<=n;i++)  v[p[i]].push_back(q[i]);
	for(int i=1;i<=n;i++)  v[q[i]].push_back(p[i]);
	for(int i=1;i<=n;i++){
		if(vis[i])  continue;
		num=0,dfs(i),fill(g,g+num+now+1,0);
		if(num==1){
			for(int j=++now;j>=1;j--)  add(f[j],f[j-1]);
			continue;
		}
		for(int i=0;i<=num;i++){
			int tmp=(w[num*2-1][i]+w[num*2-3][i-1])%mod;
			for(int j=0;j<=now;j++)  add(g[i+j],tmp*f[j]);
		}
		now+=num,copy(g,g+now+1,f);
	}
	for(int i=0,op=1;i<=n;i++,op=mod-op)  add(ans,f[i]*op%mod*fac[n-i]);
	cout<<ans<<"\n";
}

[USACO19FEB] Mowing Mischief P

首先有路径上的权值为相邻两点组成的矩形面积和。

然后发现这个玩意满足决策单调性,虽然单调性和一般题相比是反的。

按在二维 LIS 上的最大位置分层,这样每一层都是 \(x\) 单增 \(y\) 单减的。

然后发现每一个点都只能被前一层的一个区间转移到,线段树维护后再决策单调性即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
vector<int> tmp,lis[N];
int n,m,k,ans=INF,dp[N];
struct Node{int x,y;}a[N];
bool cmp(Node a,Node b){
    if(a.x==b.x)  return a.y<b.y;
    return a.x<b.x;
}
struct BIT{
    int tr[N];
    void update(int x,int y){
        while(x<=k)  tr[x]=max(tr[x],y),x+=x&-x;
    }
    int query(int x,int res=0){
        while(x)  res=max(res,tr[x]),x-=x&-x;
        return res;
    }
}tr;
struct Segment_tree{
    vector<int> pos,tr[N];
    int n,cnt,root,ls[N],rs[N];
    void build(int l,int r,int &p){
        p=++cnt,ls[cnt]=rs[cnt]=0,tr[cnt].clear();
        if(l==r)  return;
        int mid=(l+r)>>1;
        build(l,mid,ls[p]),build(mid+1,r,rs[p]);
    }
    void init(vector<int> v){
        pos=v,n=v.size(),root=cnt=0,build(0,n-1,root);
    }
    void update(int x,int l,int r,int p){
        if(a[x].x>=a[pos[r]].x&&a[x].y>=a[pos[l]].y)
            return tr[p].push_back(x);
        if(a[x].x<=a[pos[l]].x||a[x].y<=a[pos[r]].y)  return;
        int mid=(l+r)>>1;
        update(x,l,mid,ls[p]),update(x,mid+1,r,rs[p]);
    }
    void update(int x){update(x,0,n-1,root);}
    int calc(int i,int j){
        return dp[j]+(a[i].x-a[j].x)*(a[i].y-a[j].y);
    }
    void solve(int l,int r,int pl,int pr){
        if(l>r)  return;
        int res=INF,pmid=pl,mid=(l+r)>>1,now=tmp[mid];
        for(int i=pl;i<=pr;i++){
            int tmp=calc(now,pos[i]);
            if(tmp<res)  res=tmp,pmid=i;
        }
        dp[now]=min(dp[now],res);
        solve(l,mid-1,pmid,pr),solve(mid+1,r,pl,pmid);
    }
    void work(int l,int r,int p){
        if(!tr[p].empty())  tmp=tr[p],solve(0,tmp.size()-1,l,r);
        if(l==r)  return;int mid=(l+r)>>1;
        work(l,mid,ls[p]),work(mid+1,r,rs[p]);
    }
    void work(){return work(0,n-1,root);};
}SGT;
signed main(){
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<=n;i++)  scanf("%lld%lld",&a[i].x,&a[i].y);
    sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++){
        int res=tr.query(a[i].y)+1;
        tr.update(a[i].y,res),lis[res].push_back(i),m=max(m,res);
    }
    for(auto x:lis[1])  dp[x]=a[x].x*a[x].y;
    for(int i=2;i<=m;i++){
        SGT.init(lis[i-1]);
        for(auto x:lis[i])  dp[x]=INF,SGT.update(x);
        SGT.work();
    }
    for(auto x:lis[m])  ans=min(ans,dp[x]+(k-a[x].x)*(k-a[x].y));
	printf("%lld\n",ans);
}

[USACO22OPEN] 262144 Revisited P

用的 @ANIG 的做法,但是 ta 讲得不是很清楚,所以来扩写一下题解。

\(f_{l,r}\) 表示区间 \([l,r]\) 的答案,直接区间 DP 是容易的,用决策单调性可以做到 \(O(n^2)\)

但是区间 DP 的下限复杂度显然为 \(O(n^2)\) 没有前途,考虑拆分贡献。

\(g_{i,k}\) 表示使得 \(f_{l,i}\leq k\) 的最小 \(l\),当 \(a_i>k\) 时令 \(g_{i,k}=i+1\)

这样 \(ans=\sum_{k=0}^{upV}\sum_{i-1}^n(g_{i,k}-1)\)\(upV\) 为答案值域,上界为 \(V+\lceil\log n\rceil\)

为什么上界是这个?考虑极端情况 \(a_i\) 全部 \(=V\) 时,最优策略是分治下去然后每次合并两个分治区间,分治的层数至多为 \(\lceil\log n\rceil\),每层答案增加 \(1\),所以上界 \(V+\lceil\log n\rceil\)

从小到大枚举 \(k\),当 \(k=a_i\) 时有 \(g_{i,k}=i\)

\(k\neq a_i\) 时有 \(g_{i,k}=g_{g_{i,k-1}-1,k-1}\),即在当前段前面找一段最长的 \(\leq k-1\) 的合并起来。

在线段树里面维护 \(s_i=g_{i,k}-1\) 的值,这样过后 \(k\neq a_i\) 时相当于让 \(s_i\) 变成 \(s_{s_i}\),当 \(s_i\neq s_{s_i}\) 时有意义,考虑维护哪些 \(i\) 满足 \(s_i\neq s_{s_i}\),把 \(s_i\neq s_{s_i}\)\(s_i\) 用 vector 记录下来,由于 \(s\) 序列单调,要用的时候可以直接线段树二分找出来是哪段区间。

修改 \(s_i\leftarrow s_{s_i}\) 时直接线段树区间覆盖,再看新的 \(s_i\) 要不要扔到 vector 里去,\(a_i=k\) 的地方修改过后因为 \(s_i\)\(i\) 变为了 \(i-1\),所以原本 \(s_j=i\) 的地方后续都要跟着 \(s_i\) 一起往前跳,\(i\)\(i-1\) 都应该扔进 vector 里去。

模拟实现上述过程,在每个 \(k\) 累加线段树维护出的答案,就能解决本题。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1200005
#define int long long
#define all(v) v.begin(),v.end()
int n,m,ans,a[N];vector<int> dif,v[N];
struct Segment_tree{
	int tr[N],mx[N],len[N],tag[N];
	void build(int l=0,int r=n+1,int p=1){
		len[p]=r-l+1,tag[p]=-1;
		if(l==r)  return tr[p]=mx[p]=l,void();
		int mid=(l+r)>>1,ls=p<<1,rs=p<<1|1;
		build(l,mid,ls),build(mid+1,r,rs);
		tr[p]=tr[ls]+tr[rs],mx[p]=max(mx[ls],mx[rs]);
	}
	void pushdown(int &p,int &ls,int &rs){
		if(tag[p]==-1)  return;
		work(ls,tag[p]),work(rs,tag[p]),tag[p]=-1;
	}
	int query(int x,int l=0,int r=n+1,int p=1){
		if(l==r)  return tr[p];
		int mid=(l+r)>>1,ls=p<<1,rs=p<<1|1;pushdown(p,ls,rs);
		return x<=mid?query(x,l,mid,ls):query(x,mid+1,r,rs);
	}
	int bound(int x,int l=0,int r=n+1,int p=1){
		if(l==r)  return l;
		int mid=(l+r)>>1,ls=p<<1,rs=p<<1|1;pushdown(p,ls,rs);
		return mx[ls]>=x?bound(x,l,mid,ls):bound(x,mid+1,r,rs);
	}
	void cover(int sl,int sr,int x,int l=0,int r=n+1,int p=1){
		if(sl<=l&&r<=sr)  return work(p,x);
		int mid=(l+r)>>1,ls=p<<1,rs=p<<1|1;pushdown(p,ls,rs);
		if(sl<=mid)  cover(sl,sr,x,l,mid,ls);
		if(sr>mid)  cover(sl,sr,x,mid+1,r,rs);
		tr[p]=tr[ls]+tr[rs],mx[p]=max(mx[ls],mx[rs]);
	}
	void work(int &p,int &x){tr[p]=x*len[p],mx[p]=tag[p]=x;}
}SGT;
signed main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,SGT.build();
	for(int i=1;i<=n;i++)  cin>>a[i],m=max(m,a[i]+30);
	for(int i=1;i<=n;i++)  v[a[i]].push_back(i),ans+=i;
	for(int i=1;i<=m;i++,ans+=SGT.tr[1]){
		vector<int> tmp;
		vector<tuple<int,int,int>> cov;
		sort(all(dif)),dif.resize(unique(all(dif))-dif.begin());
		for(auto it:dif){
			int l=SGT.bound(it),r=SGT.bound(it+1)-1,x=SGT.query(it);
			cov.emplace_back(l,r,x),tmp.push_back(x);
		}
		for(auto [l,r,x]:cov)  SGT.cover(l,r,x);
		dif.clear();
		for(auto x:tmp)  if(SGT.query(x)!=x)  dif.push_back(x);
		for(auto x:v[i]){
			if(SGT.query(x)<x)  continue;
			SGT.cover(x,x,x-1),dif.push_back(x);
			if(SGT.query(x-1)!=x-1)  dif.push_back(x-1);
		}
	}
	cout<<ans-(n+1)*m<<"\n";
}

[AGC017F] Zigzag

参考题解:https://www.luogu.com.cn/article/gqwnwmoe

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) ((x)&-(x))
#define add(x,y) (x)=((x)+(y))%mod
const int N=20,mod=1000000007;
int n,m,k,u,ans,pre=0,now=1,s[N][N],dp[2][1<<(N-1)];
int main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m>>k,u=(1<<(n-1)),memset(s,-1,sizeof s);
	for(int i=0,a,b,c;i<k;i++)  cin>>a>>b>>c,s[a-1][b-1]=c;
	for(int i=0;i<u;i++){
		int &fl=(dp[0][i]=1);
		for(int j=0;j<n-1;j++)  fl&=(s[0][j]<0||s[0][j]==(i>>j&1));
	}
	for(int i=1;i<m;i++)  for(int j=0;j<n-1;j++){
		for(int k=0;k<u;k++){
			if(s[i][j]!=0&&!(k>>j&1)){
				int p=lowbit(k>>j)<<j;
				add(dp[now][(k^p)|(1<<j)],dp[pre][k]);
			}
			if(s[i][j]!=1&&!(k>>j&1))  add(dp[now][k],dp[pre][k]);
			if(s[i][j]!=0&&(k>>j&1))  add(dp[now][k],dp[pre][k]);
		}
		fill(dp[pre],dp[pre]+u,0),swap(now,pre);
	}
	for(int i=0;i<u;i++)  add(ans,dp[pre][i]);return cout<<ans<<"\n",0;
}

[JOISC 2020] 最古の遺跡 3

参考题解:https://www.luogu.com.cn/article/ltr7rfvr

posted @ 2026-02-02 10:23  tkdqmx  阅读(20)  评论(0)    收藏  举报