寻雾启示
最初我想到的做法是贪心。对于 ,前若干个铁由于走的路程比较近,因此可以等铁刷出来之后再走。后面就是一次拿多个搭。 就直接等完了之后一路搭过去。但是这样子需要二分,并且细节非常多,我根本无法处理(况且我根本就不知道正确性)。
烧烤一个小时之后,我偶然发现了对于前 的数据点有 。欸, 可以拿这么多分?于是我开始考虑 DP。
DP 不需要知道最优决策点具体是哪一个,可以直接枚举后计算取最优值。设 表示搭 块且最后在 位置,所需要的最少时间。我们可以枚举之前的决策点 ,然后花 的时间返回 ,等待/不等待第 块铁。这个可以通过取 得到。
然后重新回到 花费 ,最后搭到 花费 。所以状态转移方程
初始 。现在我们就成功地获得 分了,考虑优化转移。
不难通过定义发现 肯定是单调递增的,而 随 增大而增大,所以 随 增大而增大,而 对于枚举的 来说是定值。所以我们可以二分,求出一个分界点,这个分界点右边 的取值为 ,左边 的取值为 。
将式子展开,我们只需要知道 的最大值和 的最大值。红色是我们维护的内容,黑色的是定值。
- 对于 ,根据 和 的大小关系选择 或者分界点左边第一个即可(或者直接取 省的特殊处理)。
- 对于 ,不难发现分界线随着 增大而右移,所以这是一个滑动窗口。使用单调队列维护即可。
值得注意的是,由于分界线单调递增,可以单独加个指针维护,所以可以将时间复杂度缩减至 。但是我懒得想了写的 lower_bound,时间复杂度 。
const LL kMaxN = 1e5 + 5;
LL dp[kMaxN], f[kMaxN], t, m, k, t1, t2;
deque<LL> q;
LL calc(LL i) {
return dp[i] + i * (2 * t2 - t1);
}
int main() {
cin.tie(0)->sync_with_stdio(0);
for (cin >> t; t; t--) {
cin >> m >> k >> t1 >> t2;
dp[0] = 0;
f[0] = calc(0);
q.clear();
q.push_back(0);
for (LL i = 1; i <= m; i++) {
LL p = lower_bound(f, f + i, i * k) - f;
for (; q.size() && q.front() < p; q.pop_front()) { }
// 注意 t1 和 t2 大小关系,选择更小的那一种(0 或 p-1)
dp[i] = min(p ? min(i * (k + t1), i * (k + t1) + (p - 1) * (t2 - t1)) : (LL)1e18, q.size() ? calc(q.front()) + i * t1 : (LL)1e18);
for (; q.size() && calc(q.back()) >= calc(i); q.pop_back()) { }
q.push_back(i);
cout << dp[i] << ' ';
f[i] = dp[i] + t2 * i;
}
cout << '\n';
}
return 0;
}青年晚报
发现这个变换有些复杂,因为每次变换 和 的计算方式都不同(一个加导数,一个减导数),并且每次变换之后相当于还要交换一次 和 。
,哎呀,肯定是不能模拟的了,考虑拆贡献或矩阵加速。不过数据范围 肯定是不能矩阵的了,看看能不能 拆贡献。
首先对于 为偶数的情况, 和 在变换 次后就正好对应了真正的 和 ,因为交换偶数次后不变,同时可能还会有什么规律等待发掘。对于 为奇数,跑偶数的情况后再多算一次即可。
很显然,只有高位会对低位产生贡献,因为导数的 次项取决于原函数的 次项,所以我们考虑计算第 为对第 位()的贡献,暴力枚举 正好 。
打标结果如下(每行表示进行了 次操作之后的贡献序列,初始时 次项为 ,其余为 ):
0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 -72 0 1
0 0 0 0 0 3024 0 -144 0 1
0 0 0 -60480 0 9072 0 -216 0 1
0 362880 0 -241920 0 18144 0 -288 0 1
0 1814400 0 -604800 0 30240 0 -360 0 1
0 5443200 0 -1209600 0 45360 0 -432 0 1
0 12700800 0 -2116800 0 63504 0 -504 0 1
0 25401600 0 -3386880 0 84672 0 -576 0 1
0 45722880 0 -5080320 0 108864 0 -648 0 1
0 76204800 0 -7257600 0 136080 0 -720 0 1
0 119750400 0 -9979200 0 166320 0 -792 0 1
0 179625600 0 -13305600 0 199584 0 -864 0 1首先通过模拟可以得到先加后减和先减后加得到的贡献是一样的。然后不难发现对于 往 的贡献,仅当 为偶数的时候才不为 ,并且根据 的奇偶性一正一负。
但是我们还是找不到系数的规律呀?从右往左看一看每列的第一不为 的项,去掉正负后不难发现是 、、,所以对于 到 的贡献,第一项为 。
不过下面的项我们还是不知道规律呀?注意到它们都是第一项的倍数,那这个系数是什么呢?猜测它和组合数有关,差不多就相当于在 量级元素当中选择 的量级的元素的组合数(当前位作为导数加到低位,或者直接加过去)。
实测得到是 。于是就很好搞了,预处理组合数,然后 算贡献。
最后发现一个问题: 太大了 的组合数无法处理。但是 比较小,所以直接预处理 即可,算的时候乘上 的逆元即可。
const LL kMaxN = 1e7 + 5, kMod = 1e9 + 7;
LL fac[kMaxN], inv[kMaxN], hke[kMaxN], n, m;
LL pow(LL a, LL b) {
LL res = 1;
for (; b; b /= 2) {
b % 2 && (res = res * a % kMod);
a = a * a % kMod;
}
return res;
}
void init() {
fac[0] = 1;
for (LL i = 1; i < kMaxN; i++) {
fac[i] = fac[i - 1] * i % kMod;
}
inv[kMaxN - 1] = pow(fac[kMaxN - 1], kMod - 2);
for (LL i = kMaxN - 2; i >= 0; i--) {
inv[i] = inv[i + 1] * (i + 1) % kMod;
}
hke[0] = 1;
for (LL i = 1; i <= m; i++) {
hke[i] = hke[i - 1] * (n / 2 - i + 1) % kMod;
}
}
LL comb(LL n, LL m) {
if (m > n) {
return 0;
}
return hke[m] * inv[m] % kMod;
}
void solve(const vector<LL> &f, vector<LL> &res) {
for (LL i = 0; i <= m; i++) {
LL sum = f[i];
for (LL j = i; j >= 0; j -= 2) {
if (i - j > n) {
break;
}
res[j] = (res[j] + comb(n / 2, (i - j) / 2) * sum % kMod) % kMod;
sum = sum * j % kMod * (j - 1) % kMod;
sum = (-sum + kMod) % kMod;
}
}
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
init();
vector<LL> f(m + 1), g(m + 1), wyy(m + 1), zrr(m + 1);
for (LL &i : f) {
cin >> i;
}
for (LL &i : g) {
cin >> i;
}
solve(f, wyy);
solve(g, zrr);
if (n % 2) {
for (LL i = 0; i < m; i++) {
wyy[i] = (wyy[i] - (i + 1) * wyy[i + 1] % kMod + kMod) % kMod;
zrr[i] = (zrr[i] + (i + 1) * zrr[i + 1] % kMod + kMod) % kMod;
}
swap(wyy, zrr);
}
for (LL i : wyy) {
cout << i << ' ';
}
cout << '\n';
for (LL i : zrr) {
cout << i << ' ';
}
return 0;
}寻宝游戏
没写任何代码,没有什么思路,贪心暴力也不会。去看题解,发现确实是贪心,维护区间最大和严格次大值,等会看看。
呼吸之野
采用的可持久化线段树+单调队列判断是否有已经满足条件的区间被包含,但是不知道为什么全 WA 了一分也没有,大分题目。