暑假集训D14 2023.8.8 补题

A.[USACO2.1] 三值的排序 Sorting a Three-Valued Sequence

C.P4329 [COCI2006-2007#1] Bond

\(\operatorname{Solution}\)
看到数据范围较小,容易想到是状压,但由于优化技巧掌握不好,比赛时没有A掉这道题.

\(dp[i][j]\) 表示前 \(i\) 个人中,完成任务状态为 \(j\) 的概率. 只要依次枚举前 \(i\) 个人,状态 \(j\) 和第 \(i\) 个人选的任务 \(k\) 即可. 这里我想的是通过没有完成第 \(k\) 个任务的状态 \(j\) 中进行转移,也就是 if(j>>(k-1)&1)continue; dp[i][j|(1<<(k-1))] = max(dp[i][j|(1<<(k-1))],dp[i-1][j]*p[i][k]);//如果状态j右起第k位有1,就跳过.否则就通过前i-1人的状态j转移. 但其实有更好的方法,后面会讲到.

朴素版代码如下.
时间复杂度 \(O(n^2\cdot 2^n)\) ,爆
空间复杂度 \(O(n\cdot 2^n)\) ,爆

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<algorithm>
#include<math.h>
#define endl '\n'
#define pb push_back
using namespace std;
typedef pair<int,int> PII;
const int N = 1e5+10;
int p[21][21];
float dp[21][1<<20];
int n;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin>>n;
	for(int i =1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>p[i][j];
		}
	}
	for(int i = 1;i<=n;i++)
	{
		int k = (1<<(i-1)) -1;
		dp[0][k] = 1;
	}
	int m = (1<<(n)) -1;
	for(int i =1;i<=n;i++)
	{
		for(int j = 0;j<=m;j++)
		{
			dp[i][0]=1;
			for(int k = 1;k<=n;k++)
			{
				if(j>>(k-1)&1)continue;
//				dp[i][j|(1<<(k-1))] = max(dp[i][j],dp[i-1][j&(m^(1<<(k-1)))]*p[i][k]);
				dp[i][j|(1<<(k-1))] = max(dp[i][j|(1<<(k-1))],dp[i-1][j]*p[i][k]);
				//cout<<i<<" "<<(j|(1<<(k-1)))<<" "<<k<<" "<<dp[i][j]<<endl;
			}
		}
	}
	
	cout<<dp[n][m]/pow(100,n-1)<<endl;
	return 0;
}

由于只用了当前层和上一层的状态,因此可以考虑用滚动数组优化.(与上一层状态的转移不是单向的,因此不能像背包那样只用一个数组).

优化空间复杂度后,只T了两个点.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<iomanip>
#include<algorithm>
#include<math.h>
#define endl '\n'
#define pb push_back
using namespace std;
typedef pair<int,int> PII;
const int N = 1e5+10;
int p[21][21];
double backup[1<<20];
double dp[1<<20];
int n;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n;
	for(int i =1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>p[i][j];
		}
	}
	memcpy(backup,dp,sizeof dp);
	int m = (1<<(n)) -1;
	backup[0] =1;
	for(int i =1;i<=n;i++)
	{
		for(int j = 0;j<=m;j++)
		{
			dp[0]=1;
			for(int k = 1;k<=n;k++)
			{
				if(j>>(k-1)&1)continue;
				dp[j|(1<<(k-1))] = max(dp[j|(1<<(k-1))],backup[j]*p[i][k]/100);
			}
		}
		memcpy(backup,dp,sizeof dp);
	}
	cout<<fixed<<setprecision(15)<<dp[m]*100;
	return 0;
}

再度优化:
首先由于我们每轮循环限定了前 \(i\) 个人,这意味着从 \(0\) 枚举到 \(2^n-1\) 会有很多状态用不到(因为前 \(i\) 个人表示的状态有且仅有 \(i\) 个1,枚举少于 \(i\)\(1\) 的状态或者多于 \(i\)\(1\) 的状态都是无意义的.因此优化掉可以大大降低时间.

所以我们如何得到恰好有 \(i\)\(1\) 的状态们?

\(cnt[j]\) 表示 \(j\) 的二进制中 \(1\) 的个数.那么一定有 cnt[j] = cnt[j>>1] +(j&1) ,从 \(1\) 枚举到 \(2^n-1\) 即可.然后把所有满足 \(cnt = i\) 的放到 \(t[i]\) 中.当处理前 \(i\) 个人的状态时,就不用从 \(1\) 枚举到 \(2^n-1\) 了,直接遍历 \(t[i]\) 数组即可.

for(int i = 0;i<=m;i++)
{
	//		int k = (1<<(i-1)) -1;
	cnt[i] = cnt[i>>1]+(i&1);
	len[cnt[i]].pb(i);
	backup[i] =1;
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<iomanip>
#include<algorithm>
#include<math.h>
#include<vector>
#define endl '\n'
#define pb push_back
using namespace std;
typedef pair<int,int> PII;
const int N = 1e5+10;
int p[21][21];
double backup[1<<20];
vector<int> len[21];
int cnt[1<<20];
double dp[1<<20];
int n;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin>>n;
	int m = (1<<(n)) -1;
	for(int i =1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>p[i][j];
		}
	}
	for(int i = 0;i<=m;i++)
	{
		cnt[i] = cnt[i>>1]+(i&1);
		len[cnt[i]].pb(i);
		backup[i] =1;
	}
	for(int i =1;i<=n;i++)
	{
		for(int j:len[i])
		{
			dp[0]=1;
			for(int k = 1;k<=n;k++)
			{
				if(j>>(k-1)&1)continue;
				dp[j|(1<<(k-1))] = max(dp[j|(1<<(k-1))],backup[j]*p[i][k]/100);
			}
		}
		memcpy(backup,dp,sizeof dp);
	}
	cout<<fixed<<setprecision(15)<<dp[m]*100;
	
	return 0;
}

另外注意, \(cout\) 输出小数时默认只有 \(6\) 位,必须手动增大位数,否则不满足题目要求.
#include<iomanip>
cout<<fixed<<setprecision(15)<<dp[m]*100;

posted @ 2023-08-08 16:59  LZH_03  阅读(4)  评论(0编辑  收藏  举报