• 作者:zifeiy
  • 标签:状压DP,子集划分DP

题目链接:https://codeforces.com/contest/1238/problem/E

题目大意:

给你一个长度为 \(n(n \le 10^5)\) 的字符串s和 \(m(m \le 20)\) ,这个字符串由前 \(m\) 个小写字母组成。

现在你要找一个前 \(m\) 个字符的一个排列p,在这个排列p的基础上生成字符串s,并计算总代价。

代价的计算过程是:

比如我现在已经生成了字符串s的前i个字符 \(s_{1..i}\) ,现在我要生成第i+1个字符 \(s_{i+1}\) ,并且我假设 \(s_i\) 对应的字符是x,\(s_{i+1}\) 对应的字符是y,并且字符x在排列p中的位置是 \(pos_x\) ,字符y在排列p中的位置是 \(pos_y\) ,那么生成了字符 \(s_{i+1}\) 之后,总代价增加了 \(| pos_x - pos_y |\) 。

你需要找到所有排列方案中总代价最小的那个方案所对应的最小总代价。

题目分析:

首先可以看一下 官方题解

官网的解释当中涉及到了一个“subset dynamic programming”,我粗略地将它翻译为“子集动态规划”,其实可以发现这道题目的1真的跟子集有一些练习。

同时它跟状态压缩也有一些关系。

我们用i来表示每一个状态(\(0 \le i \lt 2^m\)),这个i其实对应成一个二进制数就是一个 m 位的二进制数,如果i的第j位为1就说明这个状态中已经放了第j个字符,否则就说明没有放第j个字符,那么我们就可以放第j个字符,也就是说:

通过状态 i 和字符 j 能够扩展出一个新的状态 i | (1<<j)

而且 j 放的位置也是确定的——我们可以用函数 __builtin_popcount(i) 来获取 i 的二进制表示种存在多少位为1。

然后我们这里我觉得最重要的一个点也是困扰了我很久的一个点是—— “如何消除位置的影响”

其实对于每一个状态 i 和字符 j ,如果想要从状态 i 转移到状态 i | (1<<j) ,并且我们假设 i 的二进制表示中有 c 位为1,那么我们其实可以发现,j所处的排列的位置是确定的,那就是 c (也可以是c+1,这个视你的初始坐标决定,我们这里就假设为c了)。

但是到目前为止我们还不能消除距离的影响。

我们可以枚举状态i里面的第k位:

  • 如果状态i的第k位为1,那么也就是说字符k之前已经放在了状态i的某一个位置(我们假设是 \(c_k\)),

    那么现在我们要在第c个位置放j(为了清晰起见,我们令 \(c_j\) 表示 c),

    那么k和j的代价应该是 \(cnt[j][k] \times (c_j - c_k)\) ;(因为 \(c_k \lt c_j\))
  • 如果状态i的第k位为0,那么也就是说字符k还没有状态i的某一个位置(我们假设是 \(c_k\)),

    那么现在我们要在第c个位置放j(为了清晰起见,我们令 \(c_j\) 表示 c),

    那么k和j的代价应该是 \(cnt[j][k] \times (c_k - c_j)\) 。(因为 \(c_j \lt c_k\))

所以,我们可以发现,如果我令 \(f[i]\) 表示i这个状态的最小总代价,

那么,当我们当前判断状态i和字符j的时候(并且我们假设i的第j位为0,因为此时我们可以将状态 i 加上字符 j 变到一个新的状态 i | (1<<j))。

我们一开始开一个变量 \(tmp = f[i]\) ,然后从 0 到 m-1 去遍历字符k:

  • 如果k已经存在在状态i中,则 \(tmp -= cnt[k][j] \times c\) ;
  • 如果k还没有存在在状态i中,则 \(tmp += cnt[k][j] \times c\) 。(这里的c就是之前所说的字符j放到状态i中的位置)

那么这样为什么消除了位置的影响呢?

我们假设现在放j的时候还没有放k,则我们的tmp变量减去了 \(cnt[k][j] \times c_j\) ,那么到之后去放k时候,我们的tmp变量还会加上 \(cnt[k][j] \times c_k\) ,而 \(c_k - c_j\) 其实就是它们的距离,这么一加一减就间接地处理了距离(这个点困惑了我好长时间,直到豁然开朗!)。

然后对于每一个状态i和字符j(要求状态i中的第j位为0),以及计算得到的tmp:

f[ i | (1<<j) ] = max(f[ i | (1<<j) ] , tmp);

实现代码如下:

#include <bits/stdc++.h>
using namespace std;
int n, m, f[(1<<20)], cnt[20][20];
string s;
int main() {
cin >> n >> m >> s;
for (int i = 1; i < n; i ++) {
int a = s[i-1] - 'a', b = s[i] - 'a';
if (a != b) {
cnt[a][b] ++;
cnt[b][a] ++;
}
}
fill(f+1, f+(1<<m), INT_MAX);
for (int i = 0; i < (1<<m)-1; i ++) {
int c = __builtin_popcount(i);
for (int j = 0; j < m; j ++) {
if ( !( i & (1<<j) ) ) {
int tmp = f[i];
for (int k = 0; k < m; k ++) {
if (i & (1<<k)) {
tmp += cnt[j][k] * c;
}
else { // 因为预处理cnt的时候保证cnt[x][x]==0
tmp -= cnt[j][k] * c;
}
}
f[ i | (1<<j) ] = min(f[i | (1<<j)], tmp);
}
}
}
cout << f[ (1<<m)-1 ] << endl;
return 0;
}

CF1238E.Keyboard Purchase 题解 状压/子集划分DP的更多相关文章

  1. Codeforces1238E. Keyboard Purchase(状压dp + 计算贡献)

    题目链接:传送门 思路: 题目中的m为20,而不是26,显然在疯狂暗示要用状压来做. 考虑状压字母集合.如果想要保存字母集合中的各字母的顺序,那就和经典的n!的状态的状压没什么区别了,时间复杂度为O( ...

  2. 【CF1238E】Keyboard Purchase(状压DP,贡献)

    题意:有m种小写字符,给定一个长为n的序列,定义编辑距离为序列中相邻两个字母位置差的绝对值之和,其中字母位置是一个1到m的排列 安排一种方案,求编辑距离最小 n<=1e5,m<=20 思路 ...

  3. [BZOJ 1879][SDOI 2009]Bill的挑战 题解(状压DP)

    [BZOJ 1879][SDOI 2009]Bill的挑战 Description Solution 1.考虑状压的方式. 方案1:如果我们把每一个字符串压起来,用一个布尔数组表示与每一个字母的匹配关 ...

  4. O - Matching 题解(状压dp)

    题目链接 题目大意 给你一个方形矩阵mp,边长为n(n<=21) 有n个男生和女生,如果\(mp[i][j]=1\) 代表第i个男生可以和第j个女生配对 问有多少种两两配对的方式,使得所有男生和 ...

  5. HDU 4336-Card Collector(状压,概率dp)

    题意: 有n种卡片,每包面里面,可能有一张卡片或没有,已知每种卡片在面里出现的概率,求获得n种卡片,需要吃面的包数的期望 分析: n很小,用状压,以前做状压时做过这道题,但概率怎么推的不清楚,现在看来 ...

  6. P4547 [THUWC2017]随机二分图(状压,期望DP)

    期望好题. 发现 \(n\) 非常小,应该要想到状压的. 我们可以先只考虑 0 操作. 最难的还是状态: 我们用 \(S\) 表示左部点有哪些点已经有对应点, \(T\) 表示右部点有哪些点已经有对应 ...

  7. UVA 11600-Masud Rana(状压,概率dp)

    题意: 有n个节点的图,开始有一些边存在,现在每天任意选择两点连一条边(可能已经连过),求使整个图联通的期望天数. 分析: 由于开始图可以看做几个连通分量,想到了以前做的一个题,一个点代表一个集合(这 ...

  8. UVA - 10817 Headmaster's Headache (状压类背包dp+三进制编码)

    题目链接 题目大意:有S门课程,N名在职教师和M名求职者,每名在职教师或求职者都有自己能教的课程集合以及工资,要求花费尽量少的钱选择一些人,使得每门课程都有至少两人教.在职教师必须选. 可以把“每个课 ...

  9. NOIP2017 宝藏 题解报告【状压dp】

    题目描述 参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度. 小明决心亲自前往挖掘所有宝藏屋中的宝藏.但是 ...

随机推荐

  1. 用CSS添加选中文字的背景色

  2. GIT → 05:Git命令行操作

    5.1 打开命令行窗口 安装Git后,在资源管理器的空白处,单击鼠标右键打开窗口,点击 Git Bash Here ,打开Git命令行窗口,在窗口中可直接使用Linux命令操作: 5.2 初始化Git ...

  3. oracle习题-简单查询

    题一 1 实现将已知表中的数据插入到另一个表中 学生表:stu1 向表中插入两条数据   学生信息表2:stuinfo 将stu1表中的两条数据导入到stuinfo表中,执行下列语句 此时查看一下st ...

  4. Leetcode120.Triangle三角形最小路径和

    给定一个三角形,找出自顶向下的最小路径和.每一步只能移动到下一行中相邻的结点上. 例如,给定三角形: [ [2], [3,4], [6,5,7], [4,1,8,3] ] 自顶向下的最小路径和为 11 ...

  5. Leetcode605.Can Place Flowers种花问题

    假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有.可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去. 给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花 ...

  6. js的各种获取大小

    相信大家也经常会被js的获取大小搞得头昏脑胀,到底应该用哪种方式获取才是我要的那种大小呢 好啦,在此我帮大家整理好我知道的那些. window.screen.availHeight  获取的是当前电脑 ...

  7. Hdu 1150

    Machine Schedule Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) ...

  8. Hadoop 无法启动的问题

    最近乱搞把本来就快要挂了的hdfs又给弄坏了.问题如下, 应该是节点没有启动. [hadoop@namenode hadoop]$ hadoop dfsadmin -report Configured ...

  9. C# 配置文件 Appconfig

    WinForm或WPF应用程序有时候需要保存用户的一些配置就要用到配置文件,而微软为我们的应用程序提供了Application Configuration File,就是应用程序配置文件,可以很方便的 ...

  10. oracle包头包体

    补充说明:包头和包体可以以java的接口来理解,包头像java的接口,包体像java接口的实现类. 一 包的组成 包头(package):包头部分申明包内数据类型,常量,变量,游标,子程序和异常错误处 ...