暑假集训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;