Living-Dream 系列笔记 第56期

T1

https://www.luogu.com.cn/article/wldjmsdb

T2

观察到 \(1 \le k \le 16\),考虑状压 dp。

显然的(?),我们令 \(dp_{i,j}\) 表示买了前 \(i\) 个物品,且硬币使用状况为 \(j\) 时的最小花费。

很遗憾,这个状态是错误的。原因有二:

  • MLE。

  • \(j\) 已经可以确定花费,这与状态的定义矛盾。

这促使我们想到一种贪心策略:

对于同样的花费(即硬币的使用状况),

买到的物品越多越好(也即买到的最后物品编号越大越好)

于是我们令 \(dp_i\) 表示硬币使用状况为 \(i\) 时买到的最大的最后物品编号。

对于答案,我们取 \(dp_i=n\) 的状态,求它们的剩余钱数最大值即可。

若没有这样的状态,输出 -1 即可。

对于初始状态,令 \(dp_i=0\) 即可(因为没有负钱数)。

转移时,我们枚举当前使用的硬币编号 \(j\),在区间 \([dp_{i \operatorname{xor} 2^j}+1,n]\) 内二分出 \(a_j\) 能支付的最大区间 \([dp_{i \operatorname{xor} 2^j}+1,r]\),令 \(dp_i=\max(dp_i,r)\) 即可(需要维护个前缀和)。

(其中 \(dp_{i \operatorname{xor} 2^j}\) 为不用编号为 \(j\) 的硬币时能买到的最大的最后物品编号)

关于为什么可以二分,这是因为没有负钱数,前缀和单调不减。

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int K=20,N=1e5+5;
int k,n;
int a[K],c[N],s[N];
int dp[1<<K];

int fnd(int st,int val){
	int l=st-1,r=n+1;
	while(l+1<r){
		int mid=(l+r)>>1;
		if(s[mid]-s[st-1]<=val) l=mid;
		else r=mid;
	}
	return l;
}

signed main(){
	cin>>k>>n;
	for(int i=1;i<=k;i++) cin>>a[i];
	for(int i=1;i<=n;i++) cin>>c[i],s[i]=s[i-1]+c[i];
	for(int i=0;i<(1<<k);i++){
		for(int j=1;j<=k;j++){
			if((i>>j-1)&1){
				dp[i]=max(dp[i],fnd(dp[i^(1<<j-1)]+1,a[j]));
			}
		}
	}
	int ans=-1,f=0;
	for(int i=0;i<(1<<k);i++){
		if(dp[i]==n){
			f=1; int p=0;
			for(int j=1;j<=k;j++)
				if(!((i>>j-1)&1)) p+=a[j];
			ans=max(ans,p);
		}
	}
	if(f) cout<<ans;
	else cout<<-1;
	return 0;
}

作业 T1

观察到 \(m \le 20\),考虑状压 dp,按照乐队划分状态。

我们令 \(dp_i\) 表示 \(m\) 个乐队的排列情况为 \(i\) 时需要的最小归队人数。

此时答案显然为 \(dp_{2^m-1}\)

对于初始状态,我们令 \(dp_0=0\),其余为极大值即可。

对于转移,我们可以发现所有的排好的乐队都是从第一个位置开始站在一起的。

然后,我们再维护 \(num_i\) 表示第 \(i\) 个乐队的总人数,

\(last_i\) 表示 \(m\) 个乐队的排列情况为 \(i\) 时最后一个排好的乐队的右端点,

\(num_i\) 表示第 \(i\) 个乐队的总人数,

\(s_{i,j}\) 表示前 \(i\) 个人中属于 \(j\) 乐队的总人数。

对于最后一个排好的乐队 \(j\),我们还是从 \(dp_{i \operatorname{xor} 2^j}\) 转移而来。

同时,我们需要累加的贡献(归队的人数),就是 总人数 - 已经在 \([last_j-num_j+1,last_j]\) 这一区间中的属于 \(j\) 乐队的人,即:

\[num_j-s_{last_j,j}+s_{last_j-num_j,j} \]

(关于为什么 \(s\) 的第二个下标是 \(last_j-num_j\),这是因为前缀和左端点要减一)

code
#include<bits/stdc++.h>
using namespace std;

const int N=1e5+1,M=21;
int n,m,a[N];
int num[M],s[N][M];
int dp[1<<M];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i],a[i]--;
		for(int j=0;j<m;j++) s[i][j]=s[i-1][j];
		num[a[i]]++,s[i][a[i]]++;
	}
	memset(dp,0x3f,sizeof(dp)),dp[0]=0;
	for(int i=1;i<(1<<m);i++){
		int last=0;
		for(int j=0;j<m;j++)
			if((i>>j)&1) last+=num[j];
		for(int j=0;j<m;j++)
			if((i>>j)&1) dp[i]=min(dp[i],dp[i^(1<<j)]+num[j]-s[last][j]+s[last-num[j]][j]);
	}
	cout<<dp[(1<<m)-1];
	return 0;
}

作业 T2

观察到 \(1 \le h,w \le 11\),考虑状压 dp。

显然的,我们令 \(dp_{i,j}\) 表示放前 \(i\) 行且第 \(i\) 行放置状态为 \(j\) 时的合法方案数。

初始状态令 \(dp_{0,0}=0\) 即可。

我们把矩阵分为 \(h \times w\)\(1 \times 1\) 的格子,对于每一个格子:

  • 若它与左 / 右相邻格子形成了一个 \(1 \times 2\) 的“0 连通块”,则可以横着放置一个块。

  • 若它与上面相邻格子为一个 1 一个 0,则可以竖着放置一个块。

注:

对于后一种情形,

都是 0 时无法操作上一行所以无法竖着放置,

都是 1 时放不下所以无法竖着放置,

因此只能一个 1 一个 0 时才能竖着放置。

于是,我们枚举所有状态,

按情形 1 筛选出合法状态(用计数器统计每一段连续的 0 的个数是否为偶数个即可);

再枚举两行的状态 \(j,k\)

按情形二筛选出合法的(用 \(j \operatorname{and} k\) 是否 \(=0\) 判断,因为必须填满,所以不能存在上下都是 1 的情况),

并检验它们或起来的值(相当于将所有能竖着放置的地方都竖着放置)是否符合情形 1 即可。

套路的,我们得到转移方程

\[dp_{i,j}=dp_{i,j}+dp_{i-1,k} \]

直接做即可。

code
#include<iostream>
#include<vector>
#define int long long
using namespace std;

const int N=12;
int h,w;
int dp[N][1<<N],chk[1<<N];
vector<int> ok[1<<N];

signed main(){
	while(cin>>h>>w&&h&&w){
		for(int i=0;i<(1<<w);i++){
			int cnt=0,f=1;
			for(int j=0;j<w;j++){
				if((i>>j)&1){
					if(cnt&1){ f=0; break; }
					cnt=0;
				}
				else cnt++;
			}
			if(cnt&1) f=0;
			chk[i]=f;
		}
		for(int i=0;i<(1<<w);i++){
			ok[i].clear();
			for(int j=0;j<(1<<w);j++)
				if((i&j)==0&&chk[(i|j)])
					ok[i].push_back(j);
		}
		memset(dp,0,sizeof(dp));
		dp[0][0]=1;
		for(int i=1;i<=h;i++)
			for(int j=0;j<(1<<w);j++)
				for(int k:ok[j])
					dp[i][j]+=dp[i-1][k];
 		cout<<dp[h][0]<<'\n';
	}
	return 0;
}
posted @ 2024-05-14 19:00  _KidA  阅读(10)  评论(0)    收藏  举报