专项训练 状态压缩dp

1.[USACO13NOV] No Change G

因为硬币的数据范围极小,考虑对选择硬币的状态进行状压。状态 p 的第 i 为表示第 i 枚货币是否被选择。设 dpp表示选择硬币的状态为 p 时,可以到达的商品位置。

接下来枚举所有的状态,对于每一个状态枚举当前选择的货币 c,则有 dp[p]=max(dp[p],find(sum[dp[p(1<<c)]]+coin[c]))

其中 sum[i] 表示第1个到第 i 个商品的前缀和,coin[i] 表示第 i 个货币的价值,find(x)表示价值为x的货币从1连续购买到的最大商品的下标,可以二分查找。

最后统计答案,当dp[p]=n的时候,对于状态 p 统计第 i 为是否可选,判断没有选中的位置,即为剩余的钱。

最后取 max 即可。

代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+10;
int k,n,a[maxn],sum[maxn],dp[1<<(20)];
int ans,cnt,fl=0;
int find(int lim){//表示价值为lim的货币可以从1连续购买到的最大商品的 下标!! 
	int l=1,r=n,pos=0;
	while(l<=r){
		int mid=(l+r)>>1;
		if(sum[mid]<=lim) l=mid+1,pos=mid;
		else r=mid-1;
	}
	return pos;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>k>>n;
	for(int i=1;i<=k;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		cin>>sum[i];
		sum[i]+=sum[i-1];
	}
	for(int p=0;p<(1<<k);p++){//枚举所有选硬币的状态 
		for(int i=1;i<=k;i++){
			if(!(p&(1<<(i-1)))) continue;
			int pos=find(sum[dp[p^(1<<(i-1))]]+a[i]);
			dp[p]=max(dp[p],pos);
		}
	}
	
	for(int p=0;p<(1<<k);p++){
		if(dp[p]==n){
			fl=1;
			int cnt=0;
			for(int i=1;i<=k;i++){
				if(!(p&(1<<(i-1)))) cnt+=a[i];
			}
			ans=max(ans,cnt);
		}
	}
	if(fl==0) cout<<-1<<endl;
	else cout<<ans<<endl;
}

2.[HAOI2007] 修筑绿化带

3.CF620E New Year Tree

dfs序+状压。

对于此类修改子树的问题,很容易可以想到将它整理成线性结构,使用数据结构维护,做区间修改操作。

对原树跑一遍dfs序,对dfs序建立线段树,把颜色压成二进制存到线段树中,(因为c的范围为60,所以需要开long long !)然后做区间修改区间查询操作即可。

代码
//感觉是dfs+线段树的典题啊,怎么用状压?? 
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=4e5+10;
int n,a[maxn],m;
struct edge{
	int to,nxt;
}e[2*maxn];
int head[maxn],edgenum;
void add_edge(int u,int v){
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
} 
int in[maxn],out[maxn],tim,pos[maxn];
void dfs(int x,int fa){
	tim++;
	in[x]=tim;
	pos[tim]=x;
	for(int i=head[x];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,x);
	}
	out[x]=tim;
}

struct seg_ment_tree{
	int l,r,ans,lazy;
}tr[4*maxn];
#define lid id*2
#define rid id*2+1
void pushup(int id){
	tr[id].ans=tr[lid].ans|tr[rid].ans;
}
void pushdown(int id){
	if(tr[id].lazy!=0){
		tr[lid].lazy=tr[id].lazy;
		tr[rid].lazy=tr[id].lazy;
		tr[lid].ans=tr[id].lazy;
		tr[rid].ans=tr[id].lazy;
		tr[id].lazy=0;
	}
}
void build(int id,int l,int r){
	tr[id].l=l,tr[id].r=r;
	if(l==r){
		tr[id].ans=1ll<<(a[pos[l]]);
		return;
	}
	int mid=(l+r)/2;
	build(lid,l,mid);
	build(rid,mid+1,r);
	pushup(id);
}
void update(int id,int l,int r,int x){
	if(tr[id].l==l && tr[id].r==r){
		tr[id].ans=1ll<<x;
		tr[id].lazy=1ll<<x;
		return;
	}
	pushdown(id);
	int mid=(tr[id].l+tr[id].r)/2;
	if(r<=mid) update(lid,l,r,x);
	else if(l>mid) update(rid,l,r,x);
	else update(lid,l,mid,x),update(rid,mid+1,r,x);
	pushup(id);
}

int query(int id,int l,int r){
	if(tr[id].l==l && tr[id].r==r){
		return tr[id].ans;
	}
	pushdown(id);
	int mid=(tr[id].l+tr[id].r)/2;
	if(r<=mid) return query(lid,l,r);
	else if(l>mid) return query(rid,l,r);
	else return query(lid,l,mid)|query(rid,mid+1,r);
}

int lowbit(int x){
	return x&(-x);
}
int getsum(int x){
	int ans=0;
	for(int i=x;i>0;i-=lowbit(i)) ans++;
	return ans;
} 
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}	
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		add_edge(u,v);
		add_edge(v,u); 
	}
	dfs(1,0);
	build(1,1,n);
	for(int i=1,op,u,c;i<=m;i++){
		cin>>op;
		if(op==1){
			cin>>u>>c;
			update(1,in[u],out[u],c); 
		}
		else{
			cin>>u;
			int num=query(1,in[u],out[u]);
//			cout<<"1!!!!!!!!"<<num<<endl;
			cout<<getsum(num)<<endl;
		}
	}
} 

4.[COCI2016-2017#1] Vještica

n<=16,考虑状压。

先考虑简单情况。如果只有两个串,那么 ans=lens1+lens2pre1,2 ,其中 prei,j 表示 s1,s2 的前缀长度。

考虑将其扩展到多个字符串,令 dp[s] 表示已经插入集合为 s 的最小节点数。有 dp[s]=dp[sp]+dp[p]prei...j(i,js)

对于实现,先记录sum[i][j] 为第 i 个子串中第 j 种字符的数量,然后每种字符的最小值即为共同前缀,枚举状态转移即可。

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n;
string c;
int pre[maxn][30],sum[30][30],dp[maxn];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	memset(dp,0x3f,sizeof(dp));
	memset(pre,0x3f,sizeof(pre));
	for(int i=1;i<=n;i++){
		cin>>c;
		c=" "+c;
		for(int j=c.size()-1;j;j--) 
			sum[i][c[j]-'a'+1]++;//记录第i种串每个字母出现的次数 
		sum[i][0]=c.size()-1; 
		dp[1<<i-1]=sum[i][0];
	}
	for(int s=1;s<(1<<n);s++){//枚举每一个状态 
		for(int i=1;i<=n;i++){
			if((s>>i-1)&1){//当前状态考虑第i个串 
				for(int j=1;j<=26;j++){
					pre[s][j]=min(pre[s][j],sum[i][j]);
				} 
			}
		} 
		pre[s][0]=0;
		for(int i=1;i<=26;i++) pre[s][0]+=pre[s][i];
		for(int p=s&(s-1);p;p=(p-1)&s){
			dp[s]=min(dp[s],dp[p]+dp[p^s]-pre[s][0]);
		}
	}
	cout<<dp[(1<<n)-1]+1<<endl;
} 
posted @   Aapwp  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
我给你一个没有信仰的人的忠诚
点击右上角即可分享
微信分享提示