Loading

P6622 信号传递 做题感想

题目链接

前言

在这里分享两种的做法。
一种是我第一直觉的 模拟退火(也就是骗分)
还有一种是看题解才搞懂的神仙折半搜索加上 dp

模拟退火

众所周知,模拟退火 是我这种没脑子选手用来骗分的好算法。
具体呢就是通过不断随机,来不断接近所谓的正解。

image

不得不说,那个调参是真滴恶心,经过我不断的调参,分数在 \(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\) ,我们来看看其代价的公式。

\[val=\begin{cases} pos_y - pos_x \quad (posx \leq pos_y)\\ k\times(pos_x + pos_y)\quad(pos_x > posy) \end{cases} \]

因为如果每次都调用 \(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\) 的话参见以下的转移方程

\[f_{S\bigcup{i}}=\min\{f_{S\bigcup{i}}\ , f_s+g_{i,s}\} \]

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;
}
posted @ 2022-04-17 10:55  Aonynation  阅读(47)  评论(0编辑  收藏  举报