1.P3959 [NOIP2017 提高组] 宝藏
(1)题面
(2)解题思路
首先我们观察点的数量,发现很小,因此考虑状压Dp,然后我们考虑转移哪个状态比较好。假如我们转移的是点,那么我们需要枚举点集,是一个全排列n!,然后加上状压的复杂度n^2,妥妥T飞,那么我们考虑转移深度,枚举点的子集,利用当前集合的补集的子集进行状态转移,每一次把新加入的点看作是第i层的点,那么最后计算的答案明显会偏大,但是由于我们每一次都是取最小的转移,因此错误的答案就会被避免掉。
补集:假设有集合S,则补集为S ^ ((1 << n) - 1)
子集:假设有集合S,则子集为for(int p = S;p != 0;p = (p - 1) & S)
(3)代码实现
#include "bits/stdc++.h"
using namespace std;
const int inf = 0x3f3f3f3f;
int mat[15][15],dp[4200][15];
int main()
{
memset(dp,0x3f,sizeof(dp));
memset(mat,0x3f,sizeof(mat));
int n,m;
cin >> n >> m;
for(int i = 1;i <= m;i++) {
int u,v,c;
cin >> u >> v >> c;
u --,v --;
mat[u][v] = mat[v][u] = min(mat[u][v],c);
}
for(int i = 0;i <= n;i++) dp[1 << i][1] = 0;
int up = (1 << n),ans = inf;
for(int i = 1;i <= n;i++) {
for(int j = 0;j < up;j++) {
if(dp[j][i] == inf) continue;
//j的补集
int p = (up - 1) ^ j;
//j的补集的子集
for(int k = p;k != 0;k = (k - 1) & p) {
int v = 0,lim = false;
for(int st = 0;st < n;st++) {
if(k >> st & 1) {
int mi = inf;
for(int ed = 0;ed < n;ed++) {
if(j >> ed & 1) {
mi = min(mi,mat[st][ed]);
}
}
if(mi == inf) {
lim = true;
break;
}
v += mi;
}
}
if(!lim) {
v *= i;
dp[j | k][i + 1] = min(dp[j | k][i + 1],dp[j][i] + v);
}
}
}
}
for(int i = 1;i <= n;i++) ans = min(ans,dp[up - 1][i]);
cout << ans << endl;
return 0;
}
2.acwing 4614. 匹配价值
(1)题面
(2)解题思路
我们观察到n非常小,因此我们考虑状压Dp,提前处理好询问的答案,又看到k最大100,因此我们只考虑k <= 100以内的状态,对于m个串,我们提前状压出来属于哪一种状态,然后枚举没一种状态,然后计算两种状态之间的k值,如大于k则不需要计入答案,否则给答案+当前这个状态的串的个数。
(3)代码实现
#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 5000,M = 120;
int f[N][M],w[N],c[N];
char s[N];
int main()
{
int n,m,q;
scanf("%d%d%d",&n,&m,&q);
for(int i = 0;i < n;i++) scanf("%d",&w[i]);
for(int i = 0;i < m;i++) {
scanf("%s",s);
int wi = 0;
for(int j = 0;j < n;j++) wi |= (s[j] - '0') << j;
c[wi] ++;
}
for(int i = 0;i < (1 << n);i++) {
if(!c[i]) continue;
for(int j = 0;j < (1 << n);j++) {
int h = i ^ j,p = 0;
for(int k = 0;k < n;k++) {
if((h >> k & 1) == 0)
p += w[k];
}
if(p <= 100) f[j][p] += c[i];
}
}
for(int i = 0;i < (1 << n);i++)
for(int j = 1;j <= 100;j++)
f[i][j] += f[i][j - 1];
while(q --) {
int k,wi = 0;
scanf("%s%d",s,&k);
for(int i = 0;i < n;i++) wi |= (s[i] - '0') << i;
printf("%d\n",f[wi][k]);
}
return 0;
}
3.E - Chain Contestant (atcoder.jp)
(1)题面
(2)解题思路
考虑道顶多只有十种字符,因此我们考虑状压Dp,定义状态dp[i][j][k],用前i个字符,组成的状态为j,末尾字符为k的方案数。
对于每一次处理第i层,我们应该先把之前的状态赋值过来。
进行状态转移,对于没有第i个字符的状态,
1.若状态集中不存在第i个字符
1. 若j为0,则只能为1(表示这是第一个字符)
2. 若j不为0,则可以从i - 1的状态为j的末尾为任意的状态转移过来。
2.若存在第i个字符
则只能进行相同字符结尾的转移,即dp[i][j][x] = (dp[i][j][x] + dp[i - 1][j][x]) % mod;
(3)代码实现
#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 1 << 10,mod = 998244353;
ll dp[N][N][10];
void solve()
{
int n;
cin >> n;
string s;
cin >> s;
for(int i = 1;i <= n;i++) {
int x = s[i - 1] - 'A';
for(int j = 0;j < N;j++)
for(int k = 0;k < N;k++)
dp[i][j][k] = dp[i - 1][j][k];
for(int j = 0;j < N;j++) {
if(!(j >> x & 1)) {
if(j == 0) dp[i][j | (1 << x)][x] = (dp[i][j | (1 << x)][x] + 1) % mod;
else {
for(int k = 0;k < 10;k++) dp[i][j | (1 << x)][x] = (dp[i][j | (1 << x)][x] + dp[i - 1][j][k]) % mod;
}
}
else (dp[i][j][x] = (dp[i][j][x] + dp[i - 1][j][x])) % mod;
}
}
ll ans = 0;
for(int j = 0;j < N;j++)
for(int k = 0;k < 10;k++)
ans = (ans + dp[n][j][k]) % mod;
cout << ans << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T;
T = 1;
while(T --) {
solve();
}
return 0;
}
4.E - Magical Ornament (atcoder.jp)
(1)题目大意
给你一个序列,每一组包含两个关系,表示Ai可以到Bi,Bi可以到Ai,然后给定你一个序列,问根据上面的关系最小的能包含这个序列的最短路径是多少,如果没有输出-1。 (2)解题思路
我们观察到给定序列的长度最大为17,说明我们可以状压,预处理出以序列中的每一个点的最短路,开始状压Dp,每次枚举两个顶点,一个是不在集合中的顶点z,一个是在集合中的点j,对于不在集合中的顶点到集合中的点的距离为dis[j][z] + dp[j][i](i表示这个集合)。
(3)代码实现
#include "bits/stdc++.h"
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N = 1e5 + 10,inf = 0x3f3f3f3f;
vector <int> e[N];
int c[20],pis[20][N],dp[20][1 << 20];
void bfs(int v,int dis[])
{
memset(dis,0x3f,sizeof(pis[v]));
dis[v] = 1;
queue <PII> que;
que.push({v,1});
while(que.size()) {
PII p = que.front();
que.pop();
for(auto x:e[p.fi]) {
if(dis[x] > p.se + 1) {
dis[x] = p.se + 1;
que.push({x,dis[x]});
}
}
}
}
void solve()
{
int n,m;
cin >> n >> m;
for(int i = 1;i <= m;i++) {
int u,v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
int k;
cin >> k;
memset(dp,0x3f,sizeof(dp));
for(int i = 0;i < k;i++) {
cin >> c[i];
bfs(c[i],pis[i]);
dp[i][1 << i] = 1;
}
dp[0][0] = 0;
for(int i = 0;i < (1 << k);i++) {
for(int j = 0;j < k;j++) {
if(i >> j & 1) {
for(int z = 0;z < k;z++)
if(!(i >> z & 1))
dp[z][i | (1 << z)] = min(dp[z][i | (1 << z)],dp[j][i] + pis[j][c[z]] - 1);
}
}
}
int ans = inf;
for(int i = 0;i < k;i++) ans = min(ans,dp[i][(1 << k) - 1]);
cout << (ans == inf ? -1 : ans) << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T;
T = 1;
while(T --) {
solve();
}
return 0;
}
5.F - Close Group (atcoder.jp)
(1)题目大意
给定你n个点,m条边,你可以任意断开多条边,使得联通块的数量更少,如果发现有两个点在同一个连通块中,但是没有连边,那么这样子是不符合条件的。最后问你最小连通块的数量是多少。
(2)解题思路
看到顶点是18,肯定是状压dp之类的,首先建图,若u--->v之间右边那么G[u] |= 1 << v,表示他们可以在同一个连通块中,然后我们通过枚举顶点i和所有集合,若i这个联通块包含枚举集合,那么集合中的点和i肯定属于同一个连通块。
连边好了,我们考虑删边,枚举集合i,以及集合i的子集合j,则有在i中不在j的子集+在j子集的答案。转移方程为dp[i] = min(dp[i],dp[i ^ j] + dp[j]);
(3)代码实现
#include "bits/stdc++.h"
using namespace std;
const int N = 20;
int G[20],dp[1 << 20];
void solve()
{
int n,m;
cin >> n >> m;
while(m --) {
int u,v;
cin >> u >> v;
u --,v --;
G[u] |= 1 << v;
G[v] |= 1 << u;
}
memset(dp,0x3f,sizeof(dp));
dp[0] = 1;
for(int i = 0;i < n;i++)
for(int j = 0;j < (1 << n);j++)
if((G[i] & j) == j && dp[j] == 1)
dp[j | 1 << i] = 1;
for(int i = 0;i < (1 << n);i++)
for(int j = i;j;j = (j - 1) & i)
dp[i] = min(dp[i],dp[i ^ j] + dp[j]);
cout << dp[(1 << n) - 1] << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T;
T = 1;
while(T --) {
solve();
}
return 0;
}
6.G-Gift_第 45 届国际大学生程序设计竞赛(ICPC)亚洲区域赛(昆明) (nowcoder.com)
(1)题目大意
EQWE有n个朋友,他满足每个朋友的礼物后就可以得到一定的印象,他也可以自己做一个礼物满足第i个朋友就会得到a[i]的印象,若是买一个礼物送给别人就会获得b[i]的印象,每个朋友最多收到一个礼物,问你他最多能获得多少印象。
(2)解题思路
预处理出第i个的最大时间期限,以及第i个需要花费多少时间ci,以及能得到多少印象vi,然后状压预处理出m个东西买cnt个得到的最大价值,然后定义一个dp状态dp[i][j][k]表示为前i个人买j个在第k天最大能得到多少价值。(注意有输入坑就可以了)
若j>=0 dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j - 1][k];
若k>=需要花费的时间,并且k<=最大的期限,dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j][k - prep[i].ci] + prep[i].vi)
最后答案枚举要用多少个礼物在多少天取个最大值就行了。
(3)代码实现
#include "bits/stdc++.h"
#define rep(i,z,n) for(int i = z;i <= n; i++)
#define per(i,n,z) for(int i = n;i >= z; i--)
#define ll long long
#define db double
#define PII pair<int,int>
#define fi first
#define se second
#define yes cout << "YES" << endl;
#define no cout << "NO" << endl;
using namespace std;
const int N = 510;
int dp[2][20][400],a[20],b[20],v[N];
int td[12] = {0,31,59,90,120,151,181,212,243,273,304,334};
struct node {int need,ci,vi;} prep[N];
void solve()
{
int n,m,w;
memset(v,0,sizeof(v));
memset(dp,-0x3f,sizeof(dp));
scanf("%d%d%d",&n,&m,&w);
for(int i = 1;i <= n;i++) {
int year,mon,day;
int ci,vi;
scanf("%d-%d-%d",&year,&mon,&day);
scanf("%d%d",&ci,&vi);
//输入坑
if(mon == 2 && day == 29) {
i --;
n --;
continue;
}
prep[i] = {td[mon - 1] + day,ci,vi};
}
for(int i = 1;i <= m;i++) scanf("%d%d",&a[i],&b[i]);
for(int i = 0;i < (1 << m);i ++) {
int cnt = 0,have = 0,money = 0;
for(int j = 0;j < m;j++) {
if(i >> j & 1) {
cnt ++;
have += b[j + 1];
money += a[j + 1];
}
}
if(money > w) continue;
v[cnt] = max(v[cnt],have);
}
sort(prep + 1,prep + 1 + n,[](node &p1,node &p2){
return p1.need < p2.need;
});
dp[0][0][0] = 0;
for(int i = 1;i <= n;i++) {
int dday = prep[i].need;
for(int j = 0;j <= min(i,m);j++) {
for(int k = 0;k <= 365;k++) {
int t = i & 1;
dp[t][j][k] = max(dp[!t][j][k],dp[t][j][k]);
if(j > 0) dp[t][j][k] = max(dp[t][j][k],dp[!t][j - 1][k]);
if(k >= prep[i].ci && k <= dday) dp[t][j][k] = max(dp[t][j][k],dp[!t][j][k - prep[i].ci] + prep[i].vi);
}
}
}
int ans = 0;
for(int i = 0;i <= m;i++)
for(int j = 0;j <= 365;j++)
ans = max(ans,dp[n & 1][i][j] + v[i]);
cout << ans << endl;
}
int main()
{
int T = 1;
scanf("%d",&T);
while(T --) solve();
return 0;
}
(1)题目大意
给你n个服务器,有m个直接连接关系,现在你可以额外增加一个辅助关系,表明你可以指定一个相邻的节点为辅助点,表明你到你辅助点的直接关系有直接连边,现在问你是否存在某一个辅助链,使得每一个节点能够出发到达任意节点。
(2)解题思路
很明显,我们需要找到一个环,让所有点挂在这个环上,现在考虑枚举环,n只有20说明我们可以状压dp找环,定义dp[i][j]表示以i为终点,经过状态点集为j,且起点为lowbit(j)。我们直接用dp记录环路径,然后枚举每一个环,看环外点能否挂在环上,若都行则直接输出即可。
(3)代码实现
#include "bits/stdc++.h"
#define rep(i,z,n) for(int i = z;i <= n; i++)
#define per(i,n,z) for(int i = n;i >= z; i--)
#define PII pair<int,int>
#define fi first
#define se second
#define vi vector<int>
#define vl vector<ll>
#define pb push_back
#define sz(x) (int)x.size()
#define all(x) (x).begin(),(x).end()
using namespace std;
using ll = long long;
const int N = 21;
int g[N][N],dp[N][(1 << N) + 10],pre[N];
void solve()
{
int n,m;
cin >> n >> m;
while(m --) {
int u,v;
cin >> u >> v;
u --,v --;
g[u][v] = g[v][u] = 1;
}
memset(dp,-1,sizeof(dp));
//dp[i][j]:表示终点为i,从点集状态为j经过哪个点更新过来的
for(int i = 0;i < n;i ++) dp[i][1 << i] = 0;
for(int v = 1;v < (1 << n);v ++) {
for(int i = 0;i < n;i ++) {
for(int j = __builtin_ctz(v) + 1;j < n;j ++) {
if(v >> j & 1) continue;
if(dp[i][v] == -1 || !g[i][j]) continue;
dp[j][v ^ (1 << j)] = i;
}
}
}
for(int v = 1;v < (1 << n);v ++) {
bitset <N> mask;
for(int j = 0;j < n;j ++) {
if(v >> j & 1) {
mask[j] = 1;
}
}
bool can = true;
//从环外点向环内点转移
for(int j = 0;j < n;j ++) {
if(mask[j]) continue;
bool ok = false;
for(int i = 0;i < n;i ++) {
if(!mask[i] || !g[j][i]) continue;
ok = true;
pre[j] = i;
break;
}
if(!ok) {
can = false;
break;
}
}
if(!can) continue;
for(int j = 0;j < n;j ++) {
if(dp[j][v] != -1 && g[j][__builtin_ctz(v)]) {
cout << "Yes" << endl;
int st = __builtin_ctz(v),ed = j,now = v;
while(st != ed) {
pre[dp[ed][now]] = ed;
int p = dp[ed][now];
now ^= (1 << ed);
ed = p;
}
pre[j] = st;
for(int i = 0;i < n;i ++) cout << pre[i] + 1 << ' ';
return;
}
}
}
cout << "No" << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
8.C-厄神降临之路_牛客挑战赛68 (nowcoder.com)
(1)题目大意
(2)解题思路
考虑dp[i][j]表示最后一个经过点是i,经过得点集状态是j的最小边权,那么因为是环,我们可以把二进制最后一个点看作是环的起点,我们枚举环的时候就只需要从起点开始从小到大枚举就行了,注意算答案的时候环的大小要超过2,然后正常转移就行了。
(3)代码实现
#include<bits/stdc++.h>
#define sz(x) (int) x.size()
#define rep(i,z,n) for(int i = z;i <= n; i++)
#define per(i,n,z) for(int i = n;i >= z; i--)
#define pii pair<int,int>
#define fi first
#define se second
#define vi vector<int>
#define vl vector<ll>
#define pb push_back
#define all(x) (x).begin(),(x).end()
#define endl '\n'
#define ld long double
using namespace std;
using ll = long long;
using ull = unsigned long long;
using pil = pair<int,ll>;
const int N = 21;
const ll mod = 1e9 + 7;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const ll lim = 9e18;
int a[N];
int g[N][N];
ll dp[N][1 << N];
ll ss[1 << N];
ll ans = inf;
void solve(){
int n;
cin >> n;
rep(i,1,n) cin >> a[i];
rep(i,0,(1<<n)-1) {
rep(j,0,n-1) if(i >> j & 1) ss[i] += a[j + 1];
}
rep(i,1,n) rep(j,1,n) cin >> g[i][j];
rep(i,1,n) rep(j,0,(1<<n)) dp[i][j] = inf;
rep(i,1,n) dp[i][1 << (i - 1)] = 0;
for(int i = 1;i < (1 << n);i ++) {
int jj = __builtin_ctz(i) + 1;
for(int k = jj;k <= n;k ++) {
if(dp[k][i] >= inf) continue;
if(__builtin_popcount(i) > 2 && g[k][jj]) ans = min(ans,(g[k][jj] + dp[k][i]) * ss[i]);
for(int j = jj;j <= n;j ++) {
if(!g[j][k]) continue;
if(i >> (j - 1) & 1) continue;
ll v = dp[k][i] + g[j][k];
ll &t = dp[j][i | (1 << (j - 1))];
t = min(t,v);
}
}
}
if(ans >= inf / 2) cout << -1 << endl;
else cout << ans << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
int T = 1;
// cin >> T;
while(T --) solve();
}
持续更新中!!!