一些杂的算法
康拓展开
这个算法是用来求一个数列是这几个数字的全排列的第几个的,虽然不知道为什么要求这个,感觉没什么用,但是有一个这种算法
这个算法就是从序列的第 $ 1 $ 个数字开始,一个个往后跑,然后用这个公式算就可以了。
令 \(id[i] =\) 在 \(w[i]\) 的后面比它小的元素个数
$ ans = \sum _{i = 1} ^ {n} id[i] * (n - i)! $
然后主要就是要求出 \(id[i]\) 的值,然后如果普通求的话总的复杂度是 \(O(n ^ 2)\)的,但是这个很容易被卡掉,所以可以用树状数组优化成 \(O(nlogn)\)
5367 模板 康拓展开
void update(int x,int y) {
while (x <= n) {
tr[x] += y;
x += x & -x;
}
}
lwl query(int x) {
int sum = 0;
while (x) {
sum += tr[x];
x -= x & -x;
}
return sum;
}
for (int i = 1; i <= n; i ++) {
w[i] = fr();
update(i,1);
}
jc[1] = 1;
for (int i = 2; i <= n; i ++) {
jc[i] = jc[i - 1] * i % mod;
}
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
auto id = query(w[i]);
ans = (ans + (id - 1) * jc[n - i] % mod) % mod;
update(w[i],-1);
}
ans ++;
Permutation
逆康拓展开,用的rope
rope<int> *h;
int T = fr(); // 多测
for (int i = 1; i < N; i ++)
w[i] = i;
while (T --) {
n = fr();
w[n + 1] = 0;
h = new rope<int>(w + 1);
int t;
for (int i = 1; i <= n; i ++) {
t = fr();
fw(h -> at(t));
h -> erase(t,1);
if (i == n) continue;
kg;
}
ch;
w[n + 1] = n + 1;
}
分数规划
一般形式:对于连续的实值函数(定义在实数集上的函数,在实数集上连续)\(a(x),b(x)\),满足 $ \forall x \in S , b(x) > 0$,求
如果 $ \lambda_0 = f(x_0)$ 是最优解,那么根据定义有
所以可以构造出一个新函数
然后观察 $ g(\lambda) $ 本身的性质。
-
单调性。即 $ \forall \lambda_1,\lambda_2 \in R,\lambda_1 < \lambda_2, g(\lambda_1) < g(\lambda_2)$
-
若 $ \lambda_0 $ 为原问题的答案,那么 $ g(\lambda_0) = 0 $
所以我们就可以通过二分查找来找答案。
01分数规划
定义:
给定 \(n\) 个\(a(i),b(i)\),要求 \(a(i),b(i)\) 绑定在一起选。在其中选一些,要求:
根据上面分数规划的分析,那么就有 \(\sum a(x)-x * \sum b(x)=0\)
于是我们也可以构造除一个函数 \(G(\lambda)=\max \left(\sum (a(x)-\lambda b(x))\right)\)
接着二分答案就可以。
插空dp
把元素不停地放到序列或者什么东西中,然后分成 \(k\) 段放置,求放置的方案数。状态设置的话就设置成 \(dp[i][j]\) 表示前 \(i\) 个元素放成 \(j\) 段的方案数
考虑往其中加入一个元素的时候,有以下三种情况:
-
加入到一段中,因为可以加入到任意一段中,所以转移是: \(dp[i][j] += dp[i - 1][j] * j\)
-
将当前的元素作为新的一段与之前的段放在一起,这个时候这个新的段就是插空放,所以转移是: \(dp[i][j] += j * dp[i - 1][j - 1]\)
-
将当前的元素放在两个段之间,并且合并这两个段,转移: \(dp[i][j] += dp[i - 1][j + 1] * j\)
不知道叫什么的
给你一个序列,要求将这个序列改成一个单调不减或者严格递增的序列,要求每个数字改变的值的和最小,求这个最小值。
当题目问严格递增的时候,可以先将它转化为单调不降的序列,这个时候就可以给 \(w[i] -= i\) ,这样就可以了,然后如果最后要输出改后的序列的话,加上 \(i\) 就可以啦。
首先我们凭借直觉可以知道,如果要改变某个数,那么这个数改之后的答案一定和之前序列中有的数是一样的,然后按照这个想法可以 \(O(n ^ 2)\) 的暴力。
然后我们可以进一步思考,我们可以用一个大根堆来维护前面所有值的最大值,然后因为序列单调不降,所以在加入一个新的数的时候,如果当前数不是最大值的话,那么现在就有两种选择:
- 把当前值加到最大值,费用为 \(max - u\)
- 把最大值减到当前值,费用为 \(max - u\)
那么这个情况显然应该用第二种(因为这样后面就更容易一些),但是这个时候就需要考虑一个问题,如果这个时候前面的序列没有办法满足条件该怎么办。
那么这个时候我们看这个第二小的数 \(x\) ,设这个时候前面最大的数为 \(mx\) ,当前加进去的数为 \(w\) ,因为需要考虑这个问题,说明现在 \(x < w < mx\) ,那么为了保证这个序列还是单调不减的,就需要把 \(y\) 减到 \(w\), \(x\) 加到 \(w\),这样的代价和上面的是一样的,然后此时的最大值变成了 \(w\) ,其实就不用单独处理了。
五倍经验()
点击查看代码
int main() {
priority_queue<int> q;
n = fr();
for (int i = 1; i <= n; i ++) w[i] = fr();
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
q.push(w[i]);
if (q.top() != w[i]) {
ans += abs(q.top() - w[i]);
q.pop();
q.push(w[i]);
}
}
fw(ans);
return 0;
}
CF713C Sonya and Problem Wihtout a Legend
点击查看代码
int main() {
priority_queue<int> q;
n = fr();
for (int i = 1; i <= n; i ++) w[i] = fr();
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
w[i] -= i;
q.push(w[i]);
if (q.top() != w[i]) {
ans += abs(q.top() - w[i]);
q.pop();
q.push(w[i]);
}
}
fw(ans);
return 0;
}
P4331 [BalticOI 2004] Sequence 数字序列
点击查看代码
int main() {
priority_queue<int> q;
n = fr();
for (int i = 1; i <= n; i ++) w[i] = fr();
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
w[i] -= i;
q.push(w[i]);
if (q.top() != w[i]) {
ans += abs(q.top() - w[i]);
q.pop();
q.push(w[i]);
}
res[i] = q.top();
}
fw(ans);
ch;
for (int i = n - 1; i; i --) res[i] = min(res[i + 1],res[i]);
for (int i = 1; i <= n; i ++) {
fw(res[i] + i);
kg;
}
return 0;
}
P2893 [USACO08FEB] Making the Grade G
点击查看代码
int main() {
priority_queue<int> q;
n = fr();
for (int i = 1; i <= n; i ++) w[i] = fr();
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
q.push(w[i]);
if (q.top() != w[i]) {
ans += q.top() - w[i];
q.pop();
q.push(w[i]);
}
}
priority_queue<int,vector<int>,greater<int> > qq;
lwl res = 0;
for (int i = 1; i <= n; i ++) {
qq.push(w[i]);
if (qq.top() != w[i]) {
res += abs(qq.top() - w[i]);
qq.pop();
qq.push(w[i]);
}
}
fw(min(res,ans));
return 0;
}
点击查看代码
int main() {
priority_queue<int> q;
n = fr();
for (int i = 1; i <= n; i ++) w[i] = fr();
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
q.push(w[i]);
if (q.top() != w[i]) {
ans += q.top() - w[i];
q.pop();
q.push(w[i]);
}
}
fw(ans);
return 0;
}
模拟退火
可以用来处理多峰最值的随机化算法,可以用来处理很多问题(),具体原理就不多阐述,贴个模板。
// 自定义一个范围里面取一个值的函数
double rand(double l, double r) {
return (double)rand() / RAND_MAX * (r - l) + l;
}
lwl calc() {
// 求解函数
}
// 模拟退火主体
void sa() {
for (double t = 1e4; t >= 1e-6; t *= 0.997) {
lwl res = calc();
lwl t1 = rand(2, n), t2 = rand(2, n);
swap(d[t1],d[t2]);
lwl qwq = calc();
lwl a = qwq - res;
if (exp(-a / t) < rand(0,1)) swap(d[t1],d[t2]);
}
}
int main() {
srand(19260817);
ans = calc();
while ((double)clock() / CLOCKS_PER_SEC < 0.78) sa();
// 用了这个之后就不能用控制台了,如果要调试的话就用 freopen 就可以了
return 0;
}
珂朵莉树(ODT)
一种优美的暴力(?),用于有区间推平操作的东西。主要就是用 \(set\) 来维护一个连续的区间,然后暴力计算其它询问,因为有推平操作所以后面的复杂度会逐渐减小(如果是随机数据的话,卡是很好卡的)
结构体定义:因为要用 \(set\) 来维护,所以结构体里面要重载运算符。为了方便后面一个数也可以赋值,所以写个重载函数。后面大部分操作都是指针,而且有的操作可能还要在指针那里改变值,所以用另一种方式来定义 \(value\)
struct node {
int l,r;
mutable lwl w; // 为了后面指针的时候可以改变值
node (int l,int r = 0,lwl v = 0):l(l), r(r), w(v) {}
// 重载函数
bool operator < (const node t) const {
return l < t.l;
}
// 重载运算符
};
指针的名称 : #define SIT set<node>::iterator
主要操作:分离,推平
分离:因为通常是一个区间一个区间的操作,所以要分离出左端点和右端点的一个区间,然后这里要用到的就是 \(set\) 的 \(lowerbound\) 操作,将这个左端点和前面的区间分离开,右端点和后面的区间分离开
SIT split(int pos) {
SIT it = s.lower_bound(node(pos));
if (it != s.end() && it->l == pos) return it;
it --;
if (it->r < pos) return s.end();
int l = it->l, r = it->r;
lwl val = it->w;
s.erase(it);
s.insert(node(l,pos - 1,val));
return s.insert(node(pos,r,val)).fi;
}
推平:通过上面的分离操作将这个区间分离出来后,将左端点和右端点所属区间以及中间的区间全部删掉,然后在 \(set\) 里面加一个新的区间。
这里要先取右区间再取左区间,因为取右区间的时候有可能会影响到左区间,把先取的左区间在 \(set\) 里面对应的区间给删掉,这就会 \(RE\)
void assign(int l,int r,lwl x) {
SIT itr = split(r + 1), itl = split(l);
s.erase(itl,itr);
s.insert(node(l,r,x));
}
其它操作就是暴力计算。
例题:
Willem, Chtholly and Seniorious
朱刘算法
用于解决有向图最小树形图问题。
树形图:图中不包含环,存在一个根节点,不是任何边的终点,并且除了根节点以外所有点的入度为 \(1\)
最小树形图顾名思义就是有向图中边权和最小的树形图
步骤
这里的代码用的是邻接矩存边
- 在图中找到所有点(除根节点以外)的指向他的边中长度最短的边
for (int i = 1; i <= n; i ++) {
pre[i] = i;
for (int j = 1; j <= n; j ++) {
if (dis[j][i] < dis[pre[i]][i])
pre[i] = j;
}
}
- 判断这些边有没有构成环(可以有多种方法解决,这里有的是 tarjan)
void tarjan(int u) {
dfn[u] = low[u] = ++ timestamp;
s.push(u);
vis[u] = true;
int v = pre[u];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (vis[v]) low[u] = min(low[u], dfn[v]);
if (dfn[u] == low[u]) {
cnt ++;
while (s.size()) {
auto t = s.top();
s.pop();
vis[t] = false;
id[t] = cnt;
if (t == u) break;
}
}
}
memset(dfn,0,sizeof dfn);
timestamp = 0, cnt = 0;
for (int i = 1; i <= n; i ++)
if (!dfn[i])
tarjan(i);
- 如果没有环的话就直接计算答案然后 break,否则把所有环都缩成一个点,把环中的边权加到答案里面,然后在新图中再连边
if (cnt == n) {
for (int i = 1; i <= n; i ++)
if (i != r)
ans += dis[pre[i]][i];
break;
}
for (int i = 1; i <= n; i ++)
if (id[i] == id[pre[i]] && i != r)
ans += dis[pre[i]][i];
- 分成三种边:第一种,终点在环中,这种边的权值变为原边的权值减去环中中指向这条边的终点环的边的权值。第二种,环内的边,这种边直接删掉就好了。然后剩余的边直接按照原来的边连
for (int i = 1; i <= cnt; i ++) {
for (int j = 1; j <= cnt; j ++)
bd[i][j] = linf;
}
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n; j ++) {
if (dis[i][j] < linf && id[i] != id[j]) {
int a = id[i], b = id[j];
if (id[pre[j]] == id[j])
bd[a][b] = min(bd[a][b], dis[i][j] - dis[pre[j]][j]);
else bd[a][b] = min(bd[a][b], dis[i][j]);
}
}
}
n = cnt;
r = id[r];
memcpy(dis,bd,sizeof dis);
例题
模板题 P4716 【模板】最小树形图