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\) ,我们来看看其代价的公式。
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\) 的话参见以下的转移方程
\]
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;
}
P6622 信号传递 做题感想的更多相关文章
- HDU 2016.11.12 做题感想
细数一下这两天做过的值得总结的一些题Orz...... HDU 2571 简单dp,但是一开始WA了一发.原因很简单:没有考虑仔细. 如果指向该点的所有点权值都为负数,那就错了(我一开始默认初始值为0 ...
- P2599 [ZJOI2009]取石子游戏 做题感想
题目链接 前言 发现自己三岁时的题目都不会做. 我发现我真的是菜得真实. 正文 神仙构造,分讨题. 不敢说有构造,但是分讨我只服这道题. 看上去像是一个类似 \(Nim\) 游戏的变种,经过不断猜测结 ...
- UOJ 做题记录
UOJ 做题记录 其实我这么弱> >根本不会做题呢> > #21. [UR #1]缩进优化 其实想想还是一道非常丝播的题目呢> > 直接对于每个缩进长度统计一遍就好 ...
- C语言程序设计做题笔记之C语言基础知识(下)
C 语言是一种功能强大.简洁的计算机语言,通过它可以编写程序,指挥计算机完成指定的任务.我们可以利用C语言创建程序(即一组指令),并让计算机依指令行 事.并且C是相当灵活的,用于执行计算机程序能完成的 ...
- C语言程序设计做题笔记之C语言基础知识(上)
C语言是一种功能强大.简洁的计算机语言,通过它可以编写程序,指挥计算机完成指定的任务.我们可以利用C语言创建程序(即一组指令),并让计算机依指令行事.并且C是相当灵活的,用于执行计算机程序能完成的几乎 ...
- 屏蔽Codeforces做题时的Problem tags提示
当在Codeforces上做题的时,有时会无意撇到右侧的Problem tags边栏,但是原本并不希望能够看到它. 能否把它屏蔽了呢?答案是显然的,我们只需要加一段很短的CSS即可. span.tag ...
- ACM 做题过程中的一些小技巧。
ACM做题过程中的一些小技巧. 1.一般用C语言节约空间,要用C++库函数或STL时才用C++; cout.cin和printf.scanf最好不要混用. 2.有时候int型不够用,可以用long l ...
- [日记&做题记录]-Noip2016提高组复赛 倒数十天
写这篇博客的时候有点激动 为了让自己不颓 还是写写日记 存存模板 Nov.8 2016 今天早上买了两个蛋挞 吃了一个 然后就做数论(前天晚上还是想放弃数论 但是昨天被数论虐了 woc noip模拟赛 ...
- CodeM美团点评编程大赛复赛 做题感悟&题解
[T1] [简要题意] 长度为N的括号序列,随机确定括号的方向:对于一个已确定的序列,每次消除相邻的左右括号(右左不行),消除后可以进一步合并和消除直到不能消为止.求剩下的括号的期望.\(N \l ...
随机推荐
- PostgreSQL培训认证讲师招募
中国PostgreSQL考试认证中心 以"知"之名,携手共进!不蒂于知识共享,技术传承,更愿为讲师耕织嫁衣,助您成为PostgreSQL圈的意见领袖,用知识改变世界! 一.入选讲师 ...
- mysql常见的5种日志
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 错误日志(errorlog) 用来记录mysqlId的错误信息,如数据库 ...
- Java类型跟数据库类型的相互转换
1.自定义一个转换类型,获取数据库数据并输出数据时,把数据库的Varchar类型转为java的String[]类型 前提(1) 定义一个类VarcharToStringsHandler继承BaseTy ...
- XCTF练习题---CRYPTO---不仅仅是Morse
XCTF练习题---CRYPTO---不仅仅是Morse flag:cyberpeace{attackanddefenceworldisinteresting} 解题步骤: 1.观察题目,下载附件 2 ...
- 虚拟机VMware 安装centos、常规配置、共享文件等
安装centos7[通过vm来安装运行centos7] 一.准备工作 1.centos7 的安装镜像下载链接:http://isoredirect.centos.org/centos/7/isos/x ...
- GitStats - 统计Git所有提交记录工具
如果你是研发效能组的一员或者在从事 CI/CD 或 DevOps,除了提供基础设施,指标和数据是也是一个很重要的一环,比如需要分析下某个 Git 仓库代码提交情况: 该仓库的代码谁提交的代码最多 该仓 ...
- Linux 运维工程师面试问答录(推荐阅读)
一个执着于技术的公众号 本文整理了一些比较常见的 Linux 相关的面试题目,该问答录主要分为基础知识篇和服务器篇.内容主要涉及 Linux 基本原理.常用命令操作.服务器应用等部分的内容. Linu ...
- 接口测试使用Python装饰器
写接口case时,有时需要对cae做一些共性的操作,最典型的场景如:获取case执行时间.打印log等. 有没有一种办法来集中处理共性操作从而避免在每个case中都写相同的代码(如:每个case都需要 ...
- 以点类 Point 及平面图形类 Plane 为基础设计三角形类 Triangle
学习内容:以点类 Point 及平面图形类 Plane 为基础设计三角形类 Triangle 代码示例: import java.util.Scanner; class Point{ private ...
- 『忘了再学』Shell基础 — 20、Shell中的运算符
目录 1.Shell常用运算符 2.Shell中数值运算的方法 (1)方式一 (2)方式二 (3)方式三(推荐) 1.Shell常用运算符 Shell中常用运算符如下表: 优先级数值越大优先级越高,具 ...