HDU 3680 Naughty Fairies (Also POJ 3278 Catch That Cow)
解题报告
首先 (m > n) 直接输出 (m - n),因为你只能每次做减法。
同时这给我们提供了一条重要原则:在操作过程中不能让 (m) 比 (n) 大太多。
乘法是个很重要的操作,考虑转成二进制来看。
注意到多次乘 2 就相当于在二进制后面补零,可以把数顶到前面,因此,如果 (m) 的二进制表达是 (n) 二进制表达的前缀,那是不是可以用贪心来做?每次乘 2,遇到 1 就加 1.
似乎和减法操作没啥关系了,总感觉不对劲。
这个贪心真的可以吗?
很容易举出反例:
n = (1111111)2
m = (111)2
一种更优的操作方式是 m -> 1000 -> 10000000 -> 1111111
所以这给我们一个更重要的信息:(m) 的二进制表达变成 (n) 前缀 (+1) 的二进制表达,然后再减回来,似乎也是一个可行的方案。
但是为啥不能是 (+2,+3) 甚至加更多?
这里需要用到另一个性质:在第一个乘号之后,操作序列不可能出现连续的 (+1) 或连续的 (-1),因为 (((( imes 2) +1) + 1) Leftrightarrow ((+1) imes 2)),显然是不优的。
(+2) 甚至更多的情况实际上还是多一位或者多几位的前缀 / 前缀 (+1) 问题。
如果 (m) 不是前缀咋办?那就把它变成前缀。显然在第一次执行乘法操作之前,一定可以通过加法操作把它变成某一段前缀。
所以我们可以开始 DP 了:设 f[i][0/1]
表示把 (m) 的二进制表达变成 (n) 二进制前 (i) 位的前缀(0)/ 前缀加一(1)的最小步数。
转移需要对 (n) 当前位是 (0/1) 分别讨论。
代码实现
没写高精。
int n, m;
std::bitset<30> nn;
int dp[1000000][2]; // 把 M 变成 N 的前 i 位前缀(前缀 + 1)最少步数
void DP() {
nn = n;
int mxn = 29; while (nn[mxn] == 0) --mxn;
memset(dp, 0x3f, sizeof dp);
int tmp = 0; for (int i = 0; i <= mxn; ++i) {
tmp = tmp * 2 + nn[mxn - i];
dp[i][0] = std::abs(m - tmp);
dp[i][1] = std::abs(m - tmp - 1);
}
for (int i = 1; i <= mxn; ++i) {
if (nn[mxn - i] == 0) {
dp[i][0] = std::min(dp[i][0], std::min(dp[i - 1][0] + 1, dp[i - 1][1] + 2));
dp[i][1] = std::min(dp[i][1], std::min(dp[i - 1][0] + 2, dp[i - 1][1] + 2));
} else {
dp[i][0] = std::min(dp[i][0], std::min(dp[i - 1][0] + 2, dp[i - 1][1] + 2));
dp[i][1] = std::min(dp[i][1], std::min(dp[i - 1][0] + 2, dp[i - 1][1] + 1));
}
} cout << std::min(dp[mxn][0], dp[mxn][1] + 1) << endl;
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
// while (true) {
cin >> m >> n;
if (m >= n) cout << m - n << endl;
else DP();
// }
return 0;
}
Topcoder SRM 481 Div.1 Ticket Printers
解题报告
类似于《关路灯》一样的想法,装好程序的打印机一定是一个连续区间,于是就可以很轻松设出 dp 状态:f[l][r][stat][0/1]
表示当前安排了 ([l,r]) 的打印机的任务,选中数字的状态是 stat
,当前位置在 (l(0)r(1)),的最小时间。
最小时间?
最小什么时间?
可以发现,如果我们记录“打印的最小时间”,那么我们无法得知“走路的最小时间”;反过来同理。如果我们记录“完成任务的最小时间”,那么这两个项甚至都无法转移。这是一个状态设计很模糊的 dp,它不满足最优子结构和无后效性,两种情况对后面都有影响。
怎么办?
把 ([l, r]) 的打印时间单独抽出来。换句话说,f[l][r][stat][0/1]
表示的时间是不考虑 ([l, r]) 这段打印机的打印时间,仅考虑其他打印机打印时间包括走路时间等等一大堆的时间。我们给这段打印机定完任务之后,就不管它们了,继续走,等到其他的打印机都打完了求出最短时间了,再来看这段打印机花费的时间,两个取 (max),就是总花费时间了。
转移就枚举往左端点走还是往右端点走。
另外,知道 (l) 和 stat
可以求出 (r),这样空间可以省去一维。
代码实现
class TicketPrinters {
public:
static const int MAXN = 16 + 10;
int n, maxstat = 0;
int want[MAXN], start[MAXN], poss[MAXN];
int dp[MAXN][(1 << (16 + 1))][2];
bool vis[MAXN][(1 << (16 + 1))][2];
// 走完了 [l, r],安排完了数字状态是 stat,当前在 l(0) r(1) **且不算当前打印机工作时间** 的最小总时间
// r 可以通过 l 和 popcount 算出来
// 每个状态的答案是它下一个状态的答案和当前打印机工作时间取 max,也就是把当前打印机时间拆出来单独考虑
int search(int l, int r, int stat, int dir) {
if (stat == maxstat) return 0;
if (vis[l][stat][dir]) return dp[l][stat][dir];
vis[l][stat][dir] = true; int &ans = dp[l][stat][dir] = 0x7f7f7f7f;
int pos = dir ? r : l;
for (int now = 1; now <= n; ++now) {
if (stat & (1 << (now - 1))) continue;
if (l > 1) {
ans = std::min(ans,
// !!! the dist should be calced in the state
poss[pos] - poss[l - 1] + std::max(
search(l - 1, r, stat | (1 << (now - 1)), 0),
std::abs(want[now] - start[l - 1]) + 1
// !!! Dont forget the +1
)
);
}
if (r < n) {
ans = std::min(ans,
poss[r + 1] - poss[pos] + std::max(
search(l, r + 1, stat | (1 << (now - 1)), 1),
std::abs(want[now] - start[r + 1]) + 1
)
);
}
} return ans;
}
int minTime(int currentPrinter, std::vector <int> printerDistance, std::vector <int> startingValues, std::vector <int> wantedValues) {
++currentPrinter; // !!! notice indice 0 and 1
n = wantedValues.size(); maxstat = (1 << n) - 1;
for (int i = 1; i <= n; ++i) {
want[i] = wantedValues[i - 1];
start[i] = startingValues[i - 1];
if (i != 1) poss[i] = poss[i - 1] + printerDistance[i - 1 - 1];
}
for (int i = 1; i <= n; ++i) printf("%d%s", poss[i], i == n ? "
" : " ");
int ans = 0x7f7f7f7f;
for (int i = 1; i <= n; ++i) {
ans = std::min(ans,
std::max(
search(currentPrinter, currentPrinter, (1 << (i - 1)), 0),
std::abs(want[i] - start[currentPrinter]) + 1
)
);
} return ans;
}
};