牛客竞赛 21873-牛牛的计算机内存 (状压dp(AC)贪心(WA))

博客围绕牛牛计算机内存指令执行问题展开。牛牛有m个内存块,要执行n条指令,每条指令是长度为m的01字符串,执行指令代价与新访问内存数量有关。介绍了正确的状压dp解法及状态转移方程,还分享了错误的贪心方法及代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

–>题目传送门<–
题意:
牛牛有m个内存块,现在他要执行n条指令,每条指令是长度为m的01字符串,如果s[i]=1则代表需要访问第i块内存,如果是0则不需要。每次执行一条指令所需要付出的代价为k^2, k为新访问内存的数量,即之前的指令都没有访问到的内存数量。
可以随意安排执行顺序,但必须全部执行,问最小的执行代价。

题解:
先说一下正确的解法:状压dp
题目给的数据为1至20,很明显就是告诉我们可以使用状压dp去做。
这里我们利用dp[i][j]代表执行了i条指令,n条指令的执行状态为j(j的二进制表示即为执行状态,
从低位向高位数第i位如果为1则第i条指令已被执行,为0则第i条指令未被执行)时的最小代价。
再利用一个val[i][\j]代表执行了i条指令,n条指令的执行状态为j时的内存访问情况。依旧用二进制表示第i块内存是否被访问过。(1代表曾被访问过,0表示未被访问过)。

状态转移方程为:
dp【i】【j | (1<<k)】=
min(dp【i】【j | (1<<k)】,dp【i-1】【j】+cost(val【i-1】【j】,a【k】);

注:如果是二维数组则需要使用滚动数组,否则MLE
注:可以优化为1维,每一个dp【j】保存的即为状态为j的最小代价,每一次状态转移比较时都是和前一次比较,所以相当于新值与旧值比较。
AC代码:(二维+滚动数组)

#include <bits/stdc++.h>

using namespace std;

#define LL long long
#define Re register
#define U unsigned
#define forn(i,a,b) for(Re int i = a;i <= b;++i)
#define nfor(i,a,b) for(Re int i = a;i >= b;--i)
#define CLR(i,a) memset(i,a,sizeof(i))
#define BR printf("--------------------\n")
#define pb push_back
#define Rep(i,a,b) for(int i=a;i<=b;i++)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
const double PI=atan(1.)*4.;
const int maxn=1e6+5;
const int inf = 0x3f3f3f3f;
int dp[2][1<<20],val[2][1<<22];
int a[22];
string s;

int n,len;

int cal(string s)//转换为十进制数字表示状态
{
    int res=0,b=1;
    for(int i=0;i<len;i++)
    {
        res+=(s[i]=='1'?b:0);
        b<<=1;
    }
    return res;
}

int cost(int from,int to)//计算从前一个状态转移到下一个状态所需的代价
{
    int res=0;
    for(int i=0;i<22;i++)
    {
        if((from&(1<<i))==0&&(to&(1<<i)))
            res++;
    }
    return res*res;
}


int main()
{
	ios::sync_with_stdio(false);
    cin.tie(0);
   	cout.tie(0);
   	memset(dp,inf,sizeof(dp));//初始化
    cin>>n>>len;
    for(int i=0;i<n;i++)
    {
        cin>>s;
        a[i]=cal(s);
        dp[0][1<<i]=cost(0,a[i]);//初始化,第一次执行的时候执行第i条指令的代价
        val[0][1<<i]=a[i];//同时记录改状态下内存块被访问的状态
    }
    int p,q;//滚动数组的下标
    for(int i=1;i<n;i++)
    {
        p=i&1,q=(i-1)&1;//p表示本次操作,q代表上一次操作
        for(int j=1;j<(1<<n);j++)
        {
            if(dp[q][j]==inf)continue;//如果j状态不存在
            for(int k=0;k<n;k++)
            {
                if(!(j&(1<<k)))//如果j状态下未执行第k条指令
                {
                    int tmp=dp[q][j]+cost(val[q][j],a[k]);//计算代价
                    if(tmp<dp[p][j|(1<<k)])
                    {
                        dp[p][j|(1<<k)]=tmp;
                        val[p][j|(1<<k)]=val[q][j]|a[k];//同步
                    }
                }
            }
            dp[q][j] = inf;//初始化下一次执行时,状态为j的最小代价。
        }
    }
    //最终答案为执行了n条指令,且每一条指令都被执行(j的状态为111111...)
    cout<<dp[(n-1)&1][(1<<n)-1]<<endl;
	return 0;
}

下面说一下我的贪心方法:(WA 望引以为戒)
设k为执行该指令所需访问新内存块的数量。
每一次都选取k最小的指令执行,执行过后对还未被执行过的指令更新k值(即在本次执行中访问的新内存块,在其后就不算时新的访问了),然后对于本次执行选取的指令,令其k值为1000005(相当于本题的正无穷),每次都进行排序。
每次选取k最小的执行,直至结束。

WA的贪心代码:(末尾给了错误原因加数据)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
bool vis[25];
struct node
{
    string s;
    int cnt;
}a[25];
 
bool cmp(node a,node b)
{
    return a.cnt<b.cnt;
}
int main()
{
    int n,len,ans=0;
    cin>>n>>len;
    for(int i=0;i<n;i++)
    {
        cin>>a[i].s;
        int cnt=0;
        for(int j=0;j<len;j++)
        {
            if(a[i].s[j]=='1')
                cnt++;
        }
        a[i].cnt=cnt;
    }
    for(int i=n;i<25;i++)a[i].cnt=25;
    memset(vis,false,sizeof(vis));
    sort(a,a+n,cmp);
    int zx=0;
    while(zx!=n)
    {
        ans+=a[0].cnt*a[0].cnt;
        string ss=a[0].s;
        for(int i=0;i<len;i++)
        {
            if(ss[i]=='1')
                vis[i]=true;
        }
        a[0].cnt=1000005;
        for(int i=1;i<n-zx;i++)
        {
            string ss=a[i].s;
            for(int j=0;j<len;j++)
            {
                if(ss[j]=='1'&&vis[j])
                {
                    a[i].s[j]='0';
                    a[i].cnt--;
                }
            }
        }
        zx++;
        sort(a,a+n,cmp);
    }
    cout <<ans<< endl;
    return 0;
}
/*
给组数据(贪心最小时面临同样是最小的时候做的选择不同结果也可能会不同)
3 4
1001
0110
0111
*/

欢迎评论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值