CSP-按位、移位运算专讲和状压DP(HDU-1074)

CSP-状压DP和经典作业问题

题目概述

马上假期就要结束了,zjm还有 n 个作业,完成某个作业需要一定的时间,而且每个作业有一个截止时间,若超过截止时间,一天就要扣一分。
zjm想知道如何安排做作业,使得扣的分数最少。
Tips: 如果开始做某个作业,就必须把这个作业做完了,才能做下一个作业。

Input和输入样例

有多组测试数据。第一行一个整数表示测试数据的组数
第一行一个整数 n(1<=n<=15)
接下来n行,每行一个字符串(长度不超过100) S 表示任务的名称和两个整数 D 和 C,分别表示任务的截止时间和完成任务需要的天数。
这 n 个任务是按照字符串的字典序从小到大给出。
输入样例:

2
3
Computer 3 3
English 20 1
Math 3 2
3
Computer 3 3
English 6 3
Math 6 3

Output和输出样例

每组测试数据,输出最少扣的分数,并输出完成作业的方案,如果有多个方案,输出字典序最小的一个。
输出样例:

2
Computer
Math
English
3
Computer
English
Math

按位运算和移位运算

在叙述状压DP,之前有必要说明C语言位运算中几种常用的判断和运算方法。因为不是所有人都经常使用移位运算和按位运算,先回顾一下这两种运算:
1、移位运算(以左移为例)
将给出的数字进行逻辑移位(二进制),因此每次移位低位补零,数字扩大一倍,从计算机底层原理来看移位过程时这样的:

1
10
100
1000
10000
100000
1000000

2、按位运算
按位与运算(&):
从高位到低位按位进行与运算,如果全为1,则为1,否则为0。
两种用途:
1、置零,将任何一个数和全0按位与运算,将该数清零。
2、确定指定位置上是否有数字:指定位置置1,其余置零。将原数和该数按位与,如果结果大于零则该位置上有数字。
例子:

原数:10010111
要找到第3位上是否有数字
构造的数字:100
按位与结果:00000100>0
结论:3位置上有数字

按位或运算(|):
从高位到低位按位进行或预算,如果有1,则结果为1,全0则为0。
用途:
在指定位置上置1:如果需要在某个数字的某个位置上置1,可以构造数字:置1的位置上为1,其余位置为零,将构造后的数字和原数字按位或运算。
例子:

原数:10000001
要在第45位上置1
构造的数字:11000
按位与结果:10011001
结论:45位置上成功置为了1

按位异或运算(^):
从高位到低位按位进行异或预算,相同为0,不同为1。
重要性质:某个数和0异或保持原值不变,和1异或则各位取反。
用途:
在指定位置上翻转:如果需要在某个位置上翻转,其余部分不变,可以构造数字:翻转的位置上为1,不变的位置上为0,与原数按位异或即可。

原数10011001
要在4位置上翻转
构造的数字1000
按位异或的结果:10010001
结论:4位的值完成翻转,且其余各位保持不变

实际使用:
如果S是一个01序列,每个位置上的0/1表示该位置上是否被选择,那么有如下几个位运算规则:
1、判断第 i 位是否选择:

if(S & (1<<i)) return true;
else return false;

2、将某位置的选择状态置1

S=S | (1<<i)

3、将某位置的选择状态从1置0

if(S & (1<<i))
S=S ^ (1<<i)

思路概述

了解了上面的方法,理解状压DP会变得非常简单。
状压DP解决的问题类型是:有多个事件需要选择,且每个事件是否选择会带来不同的收益,求解最优策略。在动态规划解决问题时需要模拟全部情况,传统的做法即为每一个事件都开一个维度表示,但这样往往会导致维数巨大无法求解。
状压DP利用的就是:每个事件只有0/1两种可能,这一特性可以使用二进制将其压缩,使用一个数字来表示所有物品的状态,使用该数字的某个位上的二进制0/1来表示该物品的选择状态。这样一来可以用一个数字(int),代表30-40个状态(状态数受制于二进制的数字范围)。
在进行决策时,利用位运算和移位方法,将压缩后的状态数进行运算(可以称之为解压缩过程),从而确定出目前的状态和状态转移方程。
了解了这些回看题目就变得非常简单,只有一个点需要进行叙述:字典序问题。
由于我的解法中,使用的dp数组意义为—在某个状态下最后一个选取的作业。为了保证结果是字典升序,花费相同要将字典序大的作业先安排放在作业串的最后面,dp之前字典逆序sort即可完成

实现源代码

#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;

struct each_class
{
    
    
    string cname;
    int ddl;
    int fint;
    bool operator<(each_class& a)
    {
    
    
        return cname>a.cname;
    }
};

int number=0;
const int INF=1e7;
each_class list[20];
int dp[50000];//dp存放的是状态
int ans[50000];//ans存放的是到该状态最后一个作业号

int waste_time(int num)//num对应花费的时间
{
    
    
    int an=0;
    for(int i=0;i<number;i++)
    {
    
    
        if(num & (1<<(i)))
        an+=list[i].fint;
    }
    return an;
}
int lose(int num,int k)//到num状态,最后一个完成的是k作业带来的扣分
{
    
    
    int s=0;
    s=list[k].ddl-waste_time(num);
    if(s>=0) return 0;
    else 
    return -s;
}

int main()
{
    
    
	int group=0;
	vector<string> path;
	scanf("%d",&group);
	for(int group_i=0;group_i<group;group_i++) 
	{
    
    
    int sum=0;
    scanf("%d",&number);
    for(int i=0;i<50000;i++)
    {
    
    
    	    dp[i]=INF;
    	    ans[i]=-1;
	}
    for(int i=0;i<number;i++)
    {
    
    
        cin>>list[i].cname>>list[i].ddl>>list[i].fint;
    }
    sort(list,list+number);
    dp[0]=0;
    for(int i=1;i<=(1<<number)-1;i++)
    {
    
    
        int best=0;
        for(int j=0;j<number;j++)
        {
    
    
            if(i & (1<<j))
            {
    
    
                int cnt=dp[i-(1<<j)]+lose(i,j);
                if(cnt<dp[i])
//因为是枚举最后一个完成的作业和耗费,在耗费相同时,把字典序大的作业放在后面完成可以保证
//字典序的最小,因此这里使用的sort之后的数据是字典序逆序的,先枚举字典序大的,如果符合就安排上去 
                {
    
    
                    best=j;
                    dp[i]=cnt;
                }
            }
        }
        ans[i]=best;
    }
    int num=(1<<number)-1;
    cout<<dp[num]<<endl;
    while (ans[num]!=-1)
    {
    
    
        int k=ans[num];
        path.push_back(list[k].cname);
        num-=(1<<k);
    }
    for(int i=path.size()-1;i>=0;i--)
    cout<<path[i]<<endl;
    path.clear();
	}
    return 0;
}


/*
4
Computer 3 3
Chinese 4 4 
English 20 1
Math 3 2
*/

猜你喜欢

转载自blog.csdn.net/qq_43942251/article/details/106170084