状压DP例题

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;
}

7. Problem - E - Codeforces 

        (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();
}

 

持续更新中!!!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值