洛谷题单指南-状态压缩动态规划-CF1209E2 Rotate Columns (hard version)

原题链接:https://www.luogu.com.cn/problem/CF1209E2

题意解读:n*m的数字矩阵,每列可以任意滚动,求每行最大数之和的最大值。

解题思路:

1、初步分析

由于一共只取n个数,显然这n个数尽可能大,虽然列数m比较大,但是实际用到的,必然是列最大值排名靠前的哪些列,最多需要n列即可。

反证法:如果用到的列不是列最大值前n的,必然有一列可以将用到的列替换出来,滚动后这个更大的值可以使得结果更大。

由于只需要用到列最大值前n的列,如果n足够小,比如该题的easy版本,可以直接暴搜解决,但这里的n最大12,暴搜复杂度在12^12,依然不行,可以想到状态压缩。

2、问题转化

此时,问题可以转化成:将每列数据按列最大值从大到小排序,在前min(n,m)列中选n个数覆盖所有行,使得这些数之和最大。

f[i][j]表示从前i列选的值中,覆盖行的状态为j时的最大和,j的第k位为1说明选了第k行

g[i][j]表示从第i列选的值中,覆盖行的状态为j时的最大和,j的第k位为1说明选了第k行

对于g的计算,初始没有滚动时,可以根据前n列每一列每一行的值对某一个状态是否有贡献,来进行累加:

for(int i = 0; i < m; i++) //只选前n列
    for(int j = 0; j < 1 << n; j++) //枚举第i列所有选择行的状态
        for(int k = 0; k < n; k++) //枚举选择行状态中是否包括第k行
            if(j & 1 << k)
                g[i][j] += col[i][k];

考虑到列可以随意滚动,在考虑所有可能的滚动方式之后,更新g的最大值:

for(int i = 0; i < m; i++)
    for(int j = 0; j < 1 << n; j++)
        for(int k = 0; k < n; k++) //列可能滚动0~n-1步
            g[i][j] = max(g[i][j], g[i][(j << n - k | j >> k) & ((1 << n) - 1)]);

对于f的计算,需要考虑当前列中选择行或者不选择行的情况:

for(int i = 1; i <= m; i++) //枚举所有列
    for(int j = 0; j < 1 << n; j++) //枚举前i列选择行的所有状态
    {
        f[i][j] = f[i - 1][j]; //第i列一行都没选
        for(int k = j; k; k = (k - 1) & j) //枚举j的所有二进制子集k,作为第i列选择的行的状态
             f[i][j] = max(f[i][j], f[i - 1][j ^ k] + g[i - 1][k]); 
    }

结果:f[m][(1 << n) - 1]

100分代码:

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

const int N = 15, M = 2005;

vector<int> col[M]; //col[i][0~n-1]表示第i列所有行的值,col[i][n]表示i列最大值
int f[N][1 << N]; //f[i][j]表示从前i列选的值中,覆盖行的状态为j时的最大和,j的第k位为1说明选了第k行
int g[N][1 << N]; //g[i][j]表示从第i列选的值中,覆盖行的状态为j时的最大和,j的第k位为1说明选了第k行
int t, n, m;

void init()
{
    for(int i = 0; i < m; i++) 
    {
        col[i].clear();
        col[i].resize(n + 1);
    }
    memset(f, 0, sizeof(f));
    memset(g, 0, sizeof(g));
}

bool cmp(vector<int> &a, vector<int> &b)
{
    return a[n] > b[n]; //按每列最大值从大到小排序
}

int main()
{
    cin >> t;
    while(t--)
    {
        cin >> n >> m;
        init();
        for(int i = 0; i < n; i++)
            for(int j = 0; j < m; j++)
            {
                cin >> col[j][i];
                col[j][n] = max(col[j][n], col[j][i]);
            }
                
        
        sort(col, col + m, cmp); //按列最大值从大到小排序
        m = min(n, m); //m变成了要从col中取值最前面的列数,每列最大的n行只会在前n列出现,但如果不足n列则取m
        //先统计不滚动情况下的g
        for(int i = 0; i < m; i++) //只选前n列
            for(int j = 0; j < 1 << n; j++) //枚举第i列所有选择行的状态
                for(int k = 0; k < n; k++) //枚举选择行状态中是否包括第k行
                    if(j & 1 << k)
                        g[i][j] += col[i][k];

        //再更新滚动情况下的g
        for(int i = 0; i < m; i++)
            for(int j = 0; j < 1 << n; j++)
                for(int k = 0; k < n; k++)
                    g[i][j] = max(g[i][j], g[i][(j << n - k | j >> k) & ((1 << n) - 1)]);

        //递推f
        for(int i = 1; i <= m; i++) //枚举所有列
            for(int j = 0; j < 1 << n; j++) //枚举前i列选择行的所有状态
            {
                f[i][j] = f[i - 1][j]; //第i列一行都没选
                for(int k = j; k; k = (k - 1) & j) //枚举j的所有二进制子集k,作为第i列选择的行的状态
                     f[i][j] = max(f[i][j], f[i - 1][j ^ k] + g[i - 1][k]); 
            }

        cout << f[m][(1 << n) - 1] << endl;
    }
    return 0;
}

 

posted @ 2025-08-27 11:43  hackerchef  阅读(0)  评论(0)    收藏  举报