题目链接
我们发现,对于一个序列,随便选取一个递减的子序列(可以不在原序列连续),那么他们之间连的边,不会因为插入别的点就变化。如果我们选取一个长度为 l l l的递减子序列,那么这个子序列产生的答案(填的颜色的最小值)就是 l l l。
而不同的子序列之间,我们可以认为他们没有任何关系(因为只考虑涂哪几种,不考虑涂哪一个),所以,我们只需要考虑最长的下降子序列,就是这道题目的答案了。
#include
using namespace std;
typedef long long LL;
int n;
int a[1000005], dp[1000005];
struct Tree {
LL l, r, mx;
}t[5000005];
void pu(int ni) {
t[ni].mx = max(t[ni << 1].mx, t[ni << 1 | 1].mx);
}
void build_tree(int ni, int l, int r) {
t[ni].l = l; t[ni].r = r;
if (l == r) {
t[ni].mx = 0;
return;
}
int mid = (l + r) >> 1;
build_tree(ni << 1, l, mid);
build_tree(ni << 1 | 1, mid + 1, r);
pu(ni);
}
void fix(int ni, int l, int x) {
if (l <= t[ni].l and t[ni].r <= l) {
t[ni].mx = x;
return;
}
int mid = (t[ni].l + t[ni].r) >> 1;
if (l <= mid) fix(ni << 1, l, x);
else fix(ni << 1 | 1, l, x);
pu(ni);
}
LL query(int ni, int l, int r) {
if (l <= t[ni].l and t[ni].r <= r) {
return t[ni].mx;
}
int mid = (t[ni].l + t[ni].r) >> 1;
LL ans1 = 0, ans2 = 0;
if (l <= mid) ans1 = query(ni << 1, l, r);
if (mid < r) ans2 = query(ni << 1 | 1, l, r);
return max(ans1, ans2);
}
void main2() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
dp[i] = 0;
}
build_tree(1, 1, n);
for (int i = 1; i <= n; ++i) {
LL tmp = query(1, a[i] + 1, n);
dp[i] = tmp + 1;
fix(1, a[i], dp[i]);
}
cout << *max_element(dp + 1, dp + n + 1) << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
cin >> _;
while (_--) main2();
return 0;
}
题目链接
参考了严格鸽的题解
本来以为是经典的H-H的项链的问题,但是舒爽写完,难受超时。
大体思路相似,但是有更巧妙的方法:
模拟单调栈的运行过程,掌握每一个元素入栈后,在栈里上一个元素是什么,记为 p [ i ] p[i] p[i]。对于每一个区间 [ L , R ] [L,R] [L,R],我们要查询的就是,区间内的元素的 p [ i ] p[i] p[i]与 p [ L ] p[L] p[L]的大小关系。如果 p [ i ] > p [ L ] p[i]>p[L] p[i]>p[L],那么就不是成功的元素。即,统计区间内 p [ i ] ≤ p [ L ] p[i]\leq p[L] p[i]≤p[L]的 i i i的个数。
接下来统计这个信息的方法尤为关键,因为一旦选取的方法不合适(比如说我),就会时间复杂度起飞。
求区间内不大于 k k k的元素个数,是树状数组经典应用。我们先观察我们对树状数组的所有询问:对于每一个区间 [ L , R ] [L,R] [L,R],考察区间内部的 p [ i ] p[i] p[i]小于等于 p [ L ] p[L] p[L]的元素的个数。对于一个区间,我们要考察的就是那个时候的状态下 p r e [ R ] − p r e [ L − 1 ] pre[R]-pre[L-1] pre[R]−pre[L−1],那时的树状数组中应当只记录了 p [ i ] ≤ p [ L ] p[i]\leq p[L] p[i]≤p[L]的元素的数量。
我们按照流程从左到右向单调栈逐渐推入我们的每一个元素,然后通过 p [ i ] p[i] p[i]更新树状数组。对于每一个落在 i i i位置上的询问,我们向对应的答案输送当前情况下的查询结果。这样的总体时间复杂度就是 O ( n log n + q ) O(n\log n+q) O(nlogn+q)。
#include
using namespace std;
typedef long long LL;
const int N = 500005;
struct QUERY {
int st, x, id;
};
struct PAIR {
int x, y;
}a[N];
int n, Q, si;
int c[N], p[N], s[N], ans[N];
vector<QUERY> q[N];
int lowbit(int x) {
return x & (-x);
}
void add(int x, int k) {
while (x <= n) {
c[x] += k; x += lowbit(x);
}
}
int presum(int x) {
int ans = 0;
while (x >= 1) {
ans += c[x]; x -= lowbit(x);
}
return ans;
}
int query(int l, int r) {
return presum(r) - presum(l - 1);
}
void main2() {
cin >> n >> Q;
for (int i = 1; i <= n; ++i) {
cin >> a[i].x;
q[i].clear();
}
for (int i = 1; i <= n; ++i) {
cin >> a[i].y;
p[i] = c[i] = ans[i] = 0;
}
si = s[0] = 0;
for (int i = 1; i <= n; ++i) {
while (si and (a[i].x == a[s[si]].x or a[i].y >= a[s[si]].y)) {
--si;
}
p[i] = s[si] + 1;
s[++si] = i;
}
for (int i = 1; i <= Q; ++i) {
int x, y; cin >> x >> y;
q[x - 1].push_back({-1, p[x], i});
q[y].push_back({1, p[x], i});
}
for (int i = 1; i <= n; ++i) {
add(p[i], 1);
for (auto [x, y, z]: q[i]) {
ans[z] += x * presum(y);
}
}
for (int i = 1; i <= Q; ++i) {
cout << ans[i] << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
// cin >> _;
while (_--) main2();
return 0;
}
错误的实现过程
在我不断超时的方法中,我的方法是按照H-H的项链的题目的做法,将 p [ i ] p[i] p[i]求完,然后在树状数组将 p [ i ] p[i] p[i]位置++。然后将所有查询按照右端点从大到小排序,每移动一次右端点,将右端点所经过的所有点通过 p [ i ] p[i] p[i]对树状数组的贡献删去。这样的话,对于每一个区间 [ L , R ] [L,R] [L,R],清楚完之前的贡献之后,答案就是 R − L + 1 − q u e r y ( L , R ) R-L+1-query(L,R) R−L+1−query(L,R)。
看上去也是 O ( n log n ) O(n\log n) O(nlogn),但是常数巨大无比。
错误代码如下:
#include
using namespace std;
typedef long long LL;
int n, Q, si;
int a[500005], b[500005], c[500005], p[500005], s[500005], ans[500005];
inline LL read() {
LL x = 0, y = 1; char c = getchar();
while (c > '9' || c < '0') { if (c == '-') y = -1; c = getchar(); }
while (c>='0'&&c<='9') { x=x*10+c-'0';c=getchar(); } return x*y;
}
int lowbit(int x) {
return x & (-x);
}
void add(int x, int k) {
while (x <= n) {
c[x] += k; x += lowbit(x);
}
}
int presum(int x) {
int ans = 0;
while (x >= 1) {
ans += c[x]; x -= lowbit(x);
}
return ans;
}
int query(int l, int r) {
return presum(r) - presum(l - 1);
}
struct QUERY {
int l, r, id;
}q[500005];
void main2() {
n = read(), Q = read();
for (int i = 1; i <= n; ++i) {
a[i] = read();
}
for (int i = 1; i <= n; ++i) {
b[i] = read();
}
si = s[0] = 0;
for (int i = 1; i <= n; ++i) {
while (si and (a[i] == a[s[si]] or b[i] >= b[s[si]])) {
--si;
}
p[i] = s[si];
s[++si] = i;
}
for (int i = 1; i <= Q; ++i) {
q[i].l = read(); q[i].r = read();
q[i].id = i;
}
for (int i = 1; i <= n; ++i) {
if (p[i] > 0) add(p[i], 1);
}
sort(q + 1, q + Q + 1, [](const QUERY &A, const QUERY &B) {
return A.r > B.r;
});
int cur = n;
for (int i = 1; i <= Q; ++i) {
if (q[i].r < cur) {
for (int j = cur; j > q[i].r; --j) {
add(p[j], -1);
}
cur = q[i].r;
}
ans[q[i].id] = (q[i].r - q[i].l + 1) - query(q[i].l, q[i].r);
}
for (int i = 1; i <= Q; ++i) {
printf("%d\n", ans[i]);
}
}
int main() {
LL _ = 1;
// cin >> _;
while (_--) main2();
return 0;
}
题目链接
参考了严格鸽的文章
考虑题目中操作的性质,可见,对于原序列的差分数组,我们是对两个相等的数进行合并,即如果原差分数组为1 2 2 4,那么经过一次操作后变成1 4 4,经过第二次操作后变成1 8。
显然在差分数组上的考虑是更容易的。我们发现,能够合并的两个数,如果把他们改写成 a × 2 b a\times 2^b a×2b和 c × 2 d c\times 2^d c×2d,那么一定有 a = c a=c a=c。
所以我们令 c n t [ i ] cnt[i] cnt[i]表示差分数组 d [ i ] = a × 2 b d[i]=a\times 2^b d[i]=a×2b中的 b b b,其中 a a a是奇数,再令 d [ i ] = a d[i]=a d[i]=a。 d [ i ] = 0 d[i]=0 d[i]=0的情况特殊考虑。
设 g [ i ] [ j ] g[i][j] g[i][j]为在区间 [ i , g [ i ] [ j ] ) [i,g[i][j]) [i,g[i][j])内差分数组可以合并为 d [ i ] × 2 j d[i]\times 2^j d[i]×2j的形式。如果不能合并成 d [ i ] × 2 j d[i]\times 2^j d[i]×2j的形式,则 g [ i ] [ j ] = − 1 g[i][j]=-1 g[i][j]=−1。我们在最初的初始化当中,将所有 g [ i ] [ j ] g[i][j] g[i][j]设为 − 1 -1 −1。
对 g [ i ] [ j ] g[i][j] g[i][j]的转移,我们考虑 [ i , g [ i ] [ j ] ) [i,g[i][j]) [i,g[i][j])能否达成,就要考虑 [ i , g [ i ] [ j − 1 ] ) [i,g[i][j-1]) [i,g[i][j−1])的区间能否合并,若 k = g [ i ] [ j − 1 ] k=g[i][j-1] k=g[i][j−1],那么同时也要判断 [ k , g [ k ] [ j − 1 ] ) [k,g[k][j-1]) [k,g[k][j−1])能否合并。这两个区间都能合并时,只有 d [ i ] d[i] d[i]和 d [ g [ i ] [ j ] ] d[g[i][j]] d[g[i][j]]相同, g [ i ] [ j ] g[i][j] g[i][j]才可以合并。
获得 g [ i ] [ j ] g[i][j] g[i][j]后,我们通过现有信息判断到原序列第 i i i个数为止,能够获得的最短序列是多少,设其为 d p [ i ] dp[i] dp[i]。首先对于 d [ i ] = 0 d[i]=0 d[i]=0的位置,我们直接可知 d p [ i + 1 ] = d p [ i ] dp[i+1]=dp[i] dp[i+1]=dp[i],因为直接合并掉了。如果 d [ i ] ≠ 0 d[i]\neq 0 d[i]=0,那么看能够形成 d [ i ] × 2 j d[i]\times 2^j d[i]×2j的 i i i中,最小的 d p [ g [ i ] [ j ] ] dp[g[i][j]] dp[g[i][j]],并令 d p [ i ] = min ( d p [ i ] , d p [ g [ i ] [ j ] ] + 1 ) dp[i]=\min(dp[i],dp[g[i][j]]+1) dp[i]=min(dp[i],dp[g[i][j]]+1)。
最后结果是 d p [ n ] dp[n] dp[n]。
#include
using namespace std;
typedef long long LL;
const int N = 300005;
const LL INF = 1e18 + 1;
LL n;
LL a[N], d[N], g[N][65], cnt[N], dp[N];
void main2() {
cin >> n;
d[0] = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
cnt[i] = d[i] = 0;
for (int j = 0; j < 64; ++j) {
g[i][j] = -1;
}
dp[i] = INF;
}
for (int i = 1; i < n; ++i) {
d[i] = a[i + 1] - a[i];
}
for (int i = 1; i < n; ++i) {
if (d[i] == 0) d[i] = INF;
else {
while (d[i] % 2 == 0) {
++cnt[i]; d[i] >>= 1;
}
}
}
for (int i = n - 1; i >= 1; --i) {
g[i][cnt[i]] = i + 1;
for (int j = cnt[i] + 1; j <= 63; ++j) {
if (g[i][j - 1] == -1) break;
if (g[i][j - 1] >= n) break;
if (d[g[i][j - 1]] != d[i]) break;
int k = g[i][j - 1];
g[i][j] = g[k][j - 1];
}
}
dp[1] = 1;
for (int i = 1; i < n; ++i) {
if (d[i] == INF and d[i + 1] == INF) {
dp[i + 1] = min(dp[i + 1], dp[i]);
}
else {
for (int j = 0; j < 64; ++j) {
if (g[i][j] != -1) {
dp[g[i][j]] = min(dp[g[i][j]], dp[i] + 1);
}
}
}
}
cout << dp[n] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
cin >> _;
while (_--) main2();
return 0;
}
题目链接
点数很少,可以考虑使用状态转移。用一个包含 n n n位的二进制数表示当前要判断的点的集合,如果第 i i i位为 1 1 1,则意味着集合中包含第 i i i个点(这里从 0 0 0开始编号)。接下来判断每一个集合能否形成一个环。如果这个集合能够形成一个环,那么一定选择任意一个点作为起点,都可以走过一条路径,不重复经过其他点地走回这个起点。所以为了方便起见,我们选择集合中编号最小的点作为起点。对于一个表示状态的二进制数 S S S,起点的编号就是 l o w b i t ( S ) lowbit(S) lowbit(S)。
设 d p [ S ] [ i ] dp[S][i] dp[S][i]表示状态为 S S S的集合中走到编号为 i i i的点的方案数。初始条件下,只有一个点的集合在这个点的位置的方案数是 1 1 1(用于初始化)。
接下来对于每一个状态,我们看能否从当前的状态向其他的状态去转移,比如说从状态 S S S上的点 u u u跑到一个不在 S S S集合内的一个新点 v v v上,那么从这里转移过去的那个状态就是 S ∣ ( 1 < < u ) S|(1<S∣(1<<u),那个状态的方案数要加上这个状态 u u u点处的方案数。
如果遍历结点的时候遍历到了这个集合的起点,那就将那个转移量加入到答案当中。
#include
using namespace std;
typedef long long LL;
int n, m;
LL ans = 0;
int lowbit(int x) {
return x & (-x);
}
LL e[22][22], dp[(1 << 22) + 5][22];
void main2() {
cin >> n >> m;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= n; ++j) {
e[i][j] = 0;
}
}
for (int i = 1; i <= m; ++i) {
int x, y;
cin >> x >> y;
--x; --y;
e[x][y] = e[y][x] = 1;
}
for (int i = 0; i < n; ++i) {
dp[(1 << i)][i] = 1;
}
for (int S = 1; S < (1 << n); ++S) {
int s = log2(lowbit(S));
for (int i = 0; i < n; ++i) {
if ((S & (1 << i)) == 0) continue;
if (e[s][i]) ans += dp[S][i];
for (int j = s + 1; j < n; ++j) {
if (!e[i][j]) continue;
if ((S & (1 << j)) > 0) continue;
dp[(S | (1 << j))][j] += dp[S][i];
}
}
}
cout << (ans - m) / 2;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
// cin >> _;
while (_--) main2();
return 0;
}
题目链接
我们发现,整个01串可以写成 a 0 a_0 a0个 0 0 0、 1 1 1个 1 1 1、 a 1 a_1 a1个 0 0 0、 1 1 1个 1 1 1、 a 2 a_2 a2个 0 0 0、 1 1 1个 1 1 1、 a 3 a_3 a3个 0 0 0……
因为 n n n的数据范围比较大,所以如果从 1 1 1开始去数会有点困难,我们可以反向思考,我们只需要数出所有不包含 1 1 1的区间个数,然后用总的区间个数去减一下就好了。
我们发现,答案就是 n ( n − 1 ) 2 − ∑ i = 0 m a i ( a i − 1 ) 2 \frac{n(n-1)}{2}-\displaystyle\sum\limits_{i=0}^{m} \frac{a_i(a_i-1)}{2} 2n(n−1)−i=0∑m2ai(ai−1)。
当每个 a i a_i ai相等或近似相等时,答案就是最大的。所谓近似相等,就是如果 0 0 0的个数 n − m n-m n−m与其区间个数 m + 1 m+1 m+1如果不是整除关系,那么会造成 a i a_i ai之间不同,而多出来的模数,让他们平均地分摊到每一个 a i a_i ai中。于是 1 1 1与 1 1 1之间的 0 0 0的个数要么是 x x x,要么是 x + 1 x+1 x+1,其中 x = ⌊ n − m m + 1 ⌋ x=\lfloor \frac{n-m}{m+1} \rfloor x=⌊m+1n−m⌋。
#include
using namespace std;
typedef long long LL;
LL n, m, p, tot;
void main2() {
cin >> n >> m;
tot = (n + 1) * n / 2;
p = (n - m) / (m + 1);
if ((n - m) % (m + 1) == 0) {
cout << tot - (m + 1) * p * (p + 1) / 2 << '\n';
}
else {
LL r = (n - m) % (m + 1);
cout << tot - r * (p + 1) * (p + 2) / 2 - (m + 1 - r) * p * (p + 1) / 2 << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
cin >> _;
while (_--) main2();
return 0;
}
题目链接
需要注意到一个非常重要的性质:如果我们想考察如何划分,就要尽量让每一个数在各自被划出来的区间里起到贡献,也就是说,我们争取划分的区间要么是单调递增的,要么是单调递减的。
可以明确的是:只有一个元素的区间是完全无效的,因为它为答案带来的贡献是 0 0 0。
上面的性质可以这样理解:假设有 3 3 3个数: 4 , 2 , 7 4,2,7 4,2,7,那么贡献产生自 2 , 7 2,7 2,7。如果 4 4 4被塞入到这个区间,那么与不塞进来的贡献是一样的,因为最值取自于 2 2 2和 7 7 7,与 4 4 4无关。他只有在向其他区间合并的时候,才可能产生一个贡献。
对于一个单调的区间,虽然最值只取于两边的数,但是想选择两边的数,中间的数也不得不取。试想从中间切开,会发现答案变小了。
带着这样的性质,我们可以考虑 D P DP DP:对于每一个数,如果这个数位于单调递增区间当中,那么从第一个数到这个数的答案是 d p [ i ] [ 0 ] dp[i][0] dp[i][0];如果这个数位于单调递减区间当中,那么从第一个数到这个数的答案是 d p [ i ] [ 1 ] dp[i][1] dp[i][1]。
分两种情况讨论:
#include
using namespace std;
typedef long long LL;
LL n;
LL a[1000005], dp[1000005][2];
void main2() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
dp[i][0] = dp[i][1] = -1e18;
}
dp[1][0] = dp[1][1] = 0;
for (int i = 2; i <= n; ++i) {
if (a[i] > a[i - 1]) {
dp[i][1] = max(dp[i - 1][0], dp[i - 1][1]);
dp[i][0] = max(dp[i - 1][1], dp[i - 1][0] + a[i] - a[i - 1]);
}
else {
dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
dp[i][1] = max(dp[i - 1][0], dp[i - 1][1] + a[i - 1] - a[i]);
}
}
cout << max(dp[n][0], dp[n][1]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
// cin >> _;
while (_--) main2();
return 0;
}
题目链接
从最简单的括号序列看起。假设我们现在要处理的括号序列是()()()()。
我们发现有 4 4 4个独立的括号序列,所以答案是 4 × ( 4 + 1 ) 2 = 10 \frac{4\times (4+1)}{2}=10 24×(4+1)=10。
现在让情况稍微复杂一点,考虑这样的括号序列:()()()(())。(在最后一个独立的括号序列里嵌套了一个独立序列)
我们发现,这种情况下,外层的 4 4 4个独立的括号序列对答案的贡献不变,依旧是 4 × ( 4 + 1 ) 2 = 10 \frac{4\times (4+1)}{2}=10 24×(4+1)=10,多出来的贡献只有第 4 4 4个独立括号中多出的合法括号序列()。这时这个多出来的独立括号序列对答案的贡献就是 1 1 1。因此此时的答案是 11 11 11。
再复杂一点?如果我们面对的括号序列是()()()(()()),将第四个独立括号序列中变成两个并列的合法括号序列。那么答案就是 10 + 3 = 13 10+3=13 10+3=13,最外层的 4 4 4个独立括号序列对答案的贡献是 10 10 10,第 4 4 4个括号里面对答案的贡献是 2 × ( 2 + 1 ) 2 = 3 \frac{2\times (2+1)}{2}=3 22×(2+1)=3。
好像有点头绪了。我们只需要统计出每一层的独立括号数量 x x x,这一层的独立括号对答案的贡献就是 x × ( x + 1 ) 2 \frac{x\times(x+1)}{2} 2x×(x+1)。如果这一层的独立括号内部仍然有独立括号,就先统计里面的贡献,然后单独算外面的贡献。
可以用dfs实现,但是有一个问题,就是题目并没有保证给定的括号序列是合法的。
为了简化实现过程,我们可以考虑用栈。我们每遇到一个左括号,就将一个左括号推入栈中,并初始化以这个左括号为一对左括号的左侧的一对括号内部,有多少个匹配的括号序列对答案产生贡献。每遇到一个右括号,需要分情况讨论,如果此时栈为空,那么这个右括号是不合法的,此时整体的答案贡献在计算到这里后应当截止于此,因为有这样一个不匹配的右括号拦在这里,这个右括号左右侧是没有办法一起计算的,所以干脆直接将这个括号的左边的贡献算进答案里,然后将这个括号及其左侧的所有括号全部删除(即从下一个括号开始从头考虑)。
为了实现这样一个过程,我们规定下标 0 0 0为虚空的左括号(读入字符串时需向右平移一个下标读入),这个虚空左括号用来统计最外层的括号数量。然后对于每一个括号进行如下操作:
#include
using namespace std;
typedef long long LL;
string str;
int a[1000005], s[1000005];
LL ans = 0, n;
void calc(LL x) {
ans += (x * (x + 1) / 2);
}
void main2() {
cin >> str;
int ai = 0, si = 0;
n = str.length();
for (char c: str) {
if (c == '(') a[++ai] = 1;
else a[++ai] = -1;
}
for (int i = 1; i <= n; ++i) {
if (si == 0 and a[i] == -1) {
calc(s[0]);
s[0] = 0;
continue;
}
if (a[i] == 1) {
s[++si] = 0;
}
else {
calc(s[si--]);
++s[si];
}
}
while (si >= 0) {
calc(s[si--]);
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
LL _ = 1;
// cin >> _;
while (_--) main2();
return 0;
}