P6622 信号传递 做题感想
前言
在这里分享两种的做法。
一种是我第一直觉的 模拟退火。(也就是骗分)
还有一种是看题解才搞懂的神仙折半搜索加上 dp 。
模拟退火
众所周知,模拟退火 是我这种没脑子选手用来骗分的好算法。
具体呢就是通过不断随机,来不断接近所谓的正解。
不得不说,那个调参是真滴恶心,经过我不断的调参,分数在 \(50\ pts \sim 75\ pts\) 之间横跳。
最后 \(75 \ pts\) 实在是吐了,所以就看题解了(bushi)。
这题的话模拟退火的思路还是很显然的。
每次模拟退火直接选出两个不相同的位置 \(x\) 和 \(y\) 交换即可。
注意:每次更新我们只需要更新 \(x\) 和 \(y\) 及其相关的节点
Code
#include <bits/stdc++.h>
#define file(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
#define quad putchar(' ')
#define Enter putchar('\n')
const int N = 100005;
const int M = 25;
const double eps = 0.0001;
#define int long long
int n, m, k, s[N], mid, left, right;
int pr[M][M], a[M], ans = 0x3f3f3f3f, nows;
std::mt19937 seed(1609745220);
template <class T> T rand(T l, T r) {
return std::uniform_int_distribution<int>(l, r)(seed);
}
inline int dis (int a, int b) {
if (a < b) return b - a;
else return (a + b) * k;
}
inline void query() {
int res = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++) {
if (i == j) continue;
res += pr[i][j] * dis(a[i], a[j]);
}
}
nows = res;
ans = std::min(ans, res);
}
inline void Del(int x) {
for (int i = 1; i <= m; i++) {
if (i == x) continue;
nows -= pr[x][i] * dis(a[x], a[i]);
nows -= pr[i][x] * dis(a[i], a[x]);
}
}
inline void add(int x) {
for (int i = 1; i <= m; i++) {
if (i == x) continue;
nows += pr[x][i] * dis(a[x], a[i]);
nows += pr[i][x] * dis(a[i], a[x]);
}
}
inline void add(int x, int y) {
nows += pr[x][y] * dis(a[x], a[y]);
nows += pr[y][x] * dis(a[y], a[x]);
}
inline void Del(int x, int y) {
nows -= pr[x][y] * dis(a[x], a[y]);
nows -= pr[y][x] * dis(a[y], a[x]);
}
inline void fire_ans() {
query();
double T = 1014919.1919191919191, deltT = 0.99992919191;
while (T > eps) {
int x = rand() % m + 1, y = rand() % m + 1;
while (x == y)
x = rand() % m + 1, y = rand() % m + 1;
int lasts = nows;
Del(x), Del(y), add(x, y);
std::swap(a[x], a[y]);
add(x), add(y), Del(x, y);
int delt = nows - lasts;
if (delt < 0)
ans = std::min(ans, nows);
else {
if (exp(-delt / T) * RAND_MAX <= rand())
nows = lasts, std::swap(a[x], a[y]);
}
T *= deltT;
}
}
signed main(void) {
// file("P6622");
srand(1609745220);
std::cin >> n >> m >> k;
if (n == 1) {
printf("0\n");
return 0;
}
for (int i = 1; i <= n; i++)
scanf("%lld", &s[i]);
for (int i = 1; i < n; i++)
pr[s[i]][s[i + 1]] ++;
for (int i = 1; i <= m; i++)
a[i] = i;
while(((double)clock())/(double)CLOCKS_PER_SEC <= 2.4)
fire_ans();
std::cout << ans << std::endl;
}
状压 DP
没脑子选手想不到这样的做法。
观察对于任意一对 \(x\) 和 \(y\) ,我们来看看其代价的公式。
因为如果每次都调用 \(S_i\) 来进行查询的话显然是不现实的。
考虑把代价转移到每一个坐标之上。
具体来说,对于最终在坐标为 \(x\) 的点,序列中每有一条 \(x\rightarrow y\) 的边,若 \(x\le y\) 则付出 \(k\) 的代价,否则付出 \(−1\) 的代价。同理对于 \(y\rightarrow x\) 的边分别付出 \(1\) 和 \(k\) 的代价。最终总代价等于每个点的代价分别乘其坐标再求和。
直接照搬题解上的话,没脑子选手不可能想出来。
显然,之后我们计算答案时只需要考虑一个点它前面的点的集合即可。
我们定义 \(num_{i,j}\) 表示在 \(S_i\) 中 \(i\rightarrow j\) 的条数。
定义 \(g_{i,mask}\) 表示当前 \(i\) 点,之前集合是 \(mask\) 时每一位的系数大小。
定义 \(f_i\) 表示集合为 \(i\) 时的最小代价。
对于 \(g_{i,mask}\) 我们直接对照一开始给出的公式计算即可。
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
if (i == j) continue;
g[i][0] += -num[i][j] + k * num[j][i];
}
} // 考虑所有的都在 i 的后面
for (int i = 0; i < m; i++) {
for (int j = 1; j < mid; j++) {
int now = lowbit(j);
int z = lg[now];
if (z >= i) z ++;
g[i][j] = g[i][j ^ now] + num[i][z] * (1 + k) + num[z][i] * (1 - k);
}
} // 考虑删掉其中的一个点,然后直接套公式
注意到,我们没有必要考虑 \(i\in S\) 的 \(g_{i,S}\) 。这样对于每个 \(i\) 而言只有 \(2^{22}\) 种 \(S\) 需要计算。
至于 \(f_i\) 的话参见以下的转移方程
Code
#include <bits/stdc++.h>
#define file(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout)
#define quad putchar(' ')
#define Enter putchar('\n')
const int N = 100005;
const int M = 25;
const int MAX = (1 << 23) + 5;
int n, m, k, s[N], num[M][M], g[M][MAX / 2];
int lg[MAX], siz[MAX], f[MAX], mid, mxmask;
inline int lowbit(int x) {
return x & -x;
}
signed main(void) {
// file("P6622");
std::cin >> n >> m >> k;
mxmask = 1 << m;
mid = mxmask >> 1;
for (int i = 1; i <= n; i++)
scanf("%d", &s[i]), s[i] --;
for (int i = 1; i < n; i++)
if (s[i] != s[i + 1])
num[s[i]][s[i + 1]] ++;
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
if (i == j) continue;
g[i][0] += -num[i][j] + k * num[j][i];
}
}
lg[0] = -1;
for (int i = 1; i < mxmask; i++) {
lg[i] = lg[i / 2] + 1;
siz[i] = siz[i / 2] + i % 2;
}
for (int i = 0; i < m; i++) {
for (int j = 1; j < mid; j++) {
int now = lowbit(j);
int z = lg[now];
if (z >= i) z ++;
g[i][j] = g[i][j ^ now] + num[i][z] * (1 + k) + num[z][i] * (1 - k);
}
}
// for (int i = 0; i < m; i++, printf("\n"))
// for (int j = 0; j < mid; j++)
// printf("%d ", g[i][j]);
memset(f, 0x3f, sizeof(f));
f[0] = 0;
int y;
for (int i = 1; i < mxmask; i++) {
for (int x = i; y = lowbit(x); x ^= y) {
int z = lg[y];
int last = i ^ y;
f[i] = std::min(f[i], f[i ^ y] + g[z][last & y - 1 | last >> z + 1 << z] * siz[i]);
}
}
std::cout << f[mxmask - 1] << std::endl;
}