2020.11.13 胡策
写在前面
一套好像是学长胡策的题。
扯一句,这个 b 的数据有问题= =
A
给定一长度为 \(n\) 的数列 \(a\),要求从 \(a\) 中选出一些数,使这些数不能相邻,最大化选出的数的乘积。
答案对 \(10^9 + 9\) 取模。
\(1\le n,a_i\le 10^6\)。
1S,128MB,O2,C++ 11。
显然 DP,设 \(f_{i,0/1}\) 表示考虑到前 \(i\) 个位置,第 \(i\) 个位置选/不选时,选出的数的最大乘积。
初始化 \(f_{0}=1\),有显然的状态转移:
发现乘积很大,额外维护一个数组储存 \(\log f\),用于比较大小,以完成 \(f_{i,0}\) 的转移。
时间复杂度 \(O(n)\)。
这里是 @Kersen 的代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define N 1000010
#define M 1010
using namespace std;
const int mod = 1e9+9;
const int inf = 0x3f3f3f3f;
int n, a[N]; ll dp[N][2];
double lna[N], f[N][2];
int read() {
int s = 0, f = 0; char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
int main() {
freopen("jsnk.in", "r", stdin);
freopen("jsnk.out", "w", stdout);
n = read();
for (int i = 1; i <= n; i++) a[i] = read(), lna[i] = log10(a[i]);
dp[0][0] = dp[0][1] = 1;
for (int i = 1; i <= n; i++) {
if (f[i - 1][0] >= f[i - 1][1])
f[i][0] = f[i - 1][0], dp[i][0] = dp[i - 1][0];
else f[i][0] = f[i - 1][1], dp[i][0] = dp[i - 1][1];
f[i][1] = f[i - 1][0] + lna[i];
dp[i][1] = (dp[i - 1][0] * 1ll * a[i]) % mod;
}
if (f[n][0] >= f[n][1]) cout << dp[n][0];
else cout << dp[n][1];
}
B
\(T\) 组数据,每次给定一张 \(n\) 个点 \(m\) 条边的无向图,边有边权。
给定 \(k\) 个关键点,求 \(k\) 个关键点两两距离的最小值。
\(1\le T\le 10\),\(1\le k\le n\le 10^5\),\(1\le m\le 10^5\),\(0\le\) 边权 \(\le 10^3\)。
3.5S,1G,O2,C++ 11。
扯一句,这个 b 卡 spfa,他还是人?
二进制分组多源最短路裸题。
枚举二进制位,根据关键点的编号这一位是否为 1 进行分组,将这一位为 1 的作为起点,跑多源最短路。统计到达这一位不为 1 的关键点的最短路长度。
由于所有关键点的编号都不同,能保证两个关键点在某一次分组中不在同一集合里,因此正确。
使用堆优化 Dijkstra
实现,时间复杂度 \(O((n+m)\log (n+m)\log k)\)。
注意常数。
//知识点:多源最短路
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 2e5 + 10;
const int kM = 5e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n, m, k, ans, pos[kN];
int e_num, head[kN], v[kM], w[kM], ne[kM];
int dis[kN];
bool vis[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Init() {
ans = kInf;
e_num = 0;
memset(head, 0, sizeof (head));
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dijkstra(int bit_) {
std::priority_queue <pr <int, int> > q;
memset(dis, 63, sizeof (dis));
memset(vis, 0, sizeof (vis));
for (int i = 1; i <= k; ++ i) {
if (i & (1 << bit_)) {
dis[pos[i]] = 0;
q.push(mp(0, pos[i]));
}
}
while (! q.empty()) {
int u_ = q.top().second;
q.pop();
if (vis[u_]) continue ;
vis[u_] = true;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
q.push(mp(-dis[v_], v_));
}
}
}
}
//=============================================================
int main() {
freopen("muzan.in", "r", stdin);
freopen("muzan.out", "w", stdout);
int T = read();
while (T --) {
Init();
n = read(), m = read(), k = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_);
AddEdge(v_, u_, w_);
}
for (int i = 1; i <= k; ++ i) pos[i] = read();
for (int bit = 0; (1 << bit) <= k; ++ bit) {
Dijkstra(bit);
for (int i = 1; i <= k; ++ i) {
if (i & (1 << bit)) continue ;
Chkmin(ans, dis[pos[i]]);
}
}
printf("%d\n", ans >= kInf ? -1 : ans);
}
return 0;
}
C
给定一只由 \(0,1,2\) 构成的数列,求有多少个区间,满足某权值的数量不多于区间长度的一半。
\(1\le n\le 5\times 10^6\)。
1.5S,128MB,O2,C++ 11。
考场 60pts
首先有一些性质:
- 一个区间可以表示成两个前缀的差。
- 对于一个不合法区间,只可能有一种权值的出现次数 大于区间长度的一半。
考虑先计算出所有区间的个数 \(\frac{n(n+1)}{2}\),再减去不合法区间个数。
由性质 2,三种不合法区间是互斥的,可分别考虑每一种权值。
先单独考虑 \(0\),若区间 \([l,r]\) 不合法,则显然有:
其中 \(\operatorname{cnt}_x(l,r)\) 表示区间 \([l,r]\) 内 \(x\) 的个数。再稍微化下式子,原式等价于:
即:
考虑这式子的意义,表示 \(0\) 的贡献为 \(-1\),\(1,2\) 的贡献为 \(1\)。
考虑将 \(0\) 的权值设为 \(-1\),\(1,2\) 的权值设为 \(1\),构造出新的数列 \(v\),则上式等价于:
再根据性质 1,维护 \(v\) 的前缀和 \(\operatorname{sum}\),把区间权值和拆成两个前缀和考虑,则上式等价于:
即对于每个 \(r\),询问有多少个 \(l-1\),满足 \(l<r\),且 \(\operatorname{sum}(r) < \operatorname{sum}(l-1)\)。
枚举 \(r\) 的同时,用权值树状数组简单维护不同的 \(\operatorname{sum}\) 的数量即可,时间复杂度 \(O(n\log n)\)。
对三种权值分别重复上述过程即可。
//知识点:瞎搞
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n, cnt[3][kN];
char s[kN];
LL ans;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace Bit {
#define lowbit(x) (x&-x)
int Lim, t[(kN << 1) + 10];
void Init() {
Lim = 2 * kN;
memset(t, 0, sizeof (t));
}
void Insert(int pos_) {
for (int i = pos_; i <= Lim; i += lowbit(i)) {
t[i] ++;
}
}
int Query(int pos_) {
int ret = 0;
for (int i = pos_; i; i -= lowbit(i)) {
ret += t[i];
}
return ret;
}
}
void Solve(int id_) {
Bit::Init();
for (int i = 0; i <= n; ++ i) {
// printf("%lld\n\n", ans);
ans -= 1ll * Bit::Query(2 * kN) - Bit::Query(cnt[id_][i] + kN);
Bit::Insert(cnt[id_][i] + kN);
}
}
//=============================================================
int main() {
freopen("dokuso.in", "r", stdin);
freopen("dokuso.out", "w", stdout);
n = read();
ans = 1ll * n * (n + 1ll) / 2;
scanf("%s", s + 1);
for (int i = 1; i <= n; ++ i) {
for (int j = 0; j <= 2; ++ j) {
cnt[j][i] = cnt[j][i - 1];
if (s[i] - '0' == j) {
cnt[j][i] --;
} else {
cnt[j][i] ++;
}
}
}
for (int i = 0; i <= 2; ++ i) Solve(i);
printf("%I64d\n", ans);
return 0;
}
100 pts
由于 \(v\) 中只有 1/-1,则 \(\operatorname{sum}\) 的相邻两项之差绝对值只可能为 1。
则树状数组查询的区间边界的差值也只可能为 \(1\),用树状数组维护显得很傻逼。
维护一下上次答案的增量,维护每种权值的数量,大力改即可。
复杂度 \(O(n)\)。
//知识点:瞎搞
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e6 + 10;
//=============================================================
int n, cnt[3][kN], sum[2 * kN];
char s[kN];
LL ans;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Solve(int id_) {
memset(sum, 0, sizeof (sum));
sum[kN] ++;
LL nowsum = 0;
for (int i = 1; i <= n; ++ i) {
if (cnt[id_][i] - cnt[id_][i - 1] < 0) {
nowsum += sum[cnt[id_][i - 1] + kN];
} else {
nowsum -= sum[cnt[id_][i] + kN];
}
ans -= nowsum;
sum[cnt[id_][i] + kN] ++;
}
}
//=============================================================
int main() {
// freopen("dokuso.in", "r", stdin);
// freopen("dokuso.out", "w", stdout);
n = read();
ans = 1ll * n * (n + 1ll) / 2;
scanf("%s", s + 1);
for (int i = 1; i <= n; ++ i) {
for (int j = 0; j <= 2; ++ j) {
cnt[j][i] = cnt[j][i - 1];
if (s[i] - '0' == j) {
cnt[j][i] --;
} else {
cnt[j][i] ++;
}
}
}
for (int i = 0; i <= 2; ++ i) Solve(i);
printf("%lld\n", ans);
return 0;
}
std
std 的化式子也挺经典的,可以看下这道题:
基本思想是通过调整不等式,使得不等式两侧只与一个下标有关。
比如这样的不等式:\(a_i+b_j>a_j+b_i\),可以调整成 \(a_i-a_j>b_i-b_j\)。
std 的推导似乎比我麻烦许多= =
跑路了!
总结
- 取 \(\log\) 以比较连乘的大小。
- 学到了二进制分组。
- 对于一类这样的不等式:\(a_i+b_j>a_j+b_i\),可以调整成 \(a_i-a_j>b_i-b_j\)。
- 维护信息时注意特殊性质,不可把问题过于抽象,可能会丢失某些重要的信息。