HNOI2018做题笔记

HNOI2018

寻宝游戏(位运算、基数排序)

看到位运算就要按位考虑。二进制下,\(\land 1\)与\(\lor 0\)没有意义的,\(\land 0\)强制这一位变为\(0\),\(\lor 1\)强制这一位变为\(1\)

那么如果某一位的答案要为\(0\),也就意味着:要么同时不存在\(\land 0\)与\(\lor 1\),要么最后一个\(\land 0\)后面不能有\(\lor 1\)。答案为\(1\)同理。

那么对于每一位,将所有\(a_i\)在这一位上的值从右往左看作一个二进制数\(x\),将操作序列\(\land\)对应为\(1\)、\(\lor\)对应为\(0\),从右往左看作一个二进制数\(op\),那么如果答案的这一位为\(0\),必须满足\(op \geq x\)。反之则要满足\(op < x\)。

那么最后的答案就会表现为\(a \leq op < b\)的形式,答案为\(\min\{b-a , 0\}\)。

那么将\(x\)排序,每一次扫一遍找到\(a\)和\(b\)就可以了。注意到给出这\(m\)个数的顺序正好是从低位到高位依次给出,所以可以基数排序。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<iomanip>
//This code is written by Itst
using namespace std; const int MOD = 1e9 + 7;
int srt[5007] , val[5007] , tmp[5007] , pot[3];
int N , M , Q;
char s[5007]; int main(){
#ifndef ONLINE_JUDGE
freopen("in","r",stdin);
//freopen("out","w",stdout);
#endif
scanf("%d %d %d" , &N , &M , &Q);
for(int i = 1 ; i <= M ; ++i)
srt[i] = i;
int times = 1;
for(int i = 1 ; i <= N ; ++i){
pot[0] = pot[1] = 0;
scanf("%s" , s + 1);
for(int j = 1 ; j <= M ; ++j){
++pot[s[j] - 47];
if(s[j] - 48)
val[j] = (val[j] + times) % MOD;
}
for(int j = 1 ; j <= M ; ++j)
tmp[++pot[s[srt[j]] - 48]] = srt[j];
memcpy(srt , tmp , sizeof(tmp));
times = times * 2 % MOD;
}
srt[M + 1] = M + 1;
val[M + 1] = times;
while(Q--){
scanf("%s" , s + 1);
int L = 0 , R = M + 1;
for(int i = 1 ; i <= M ; ++i)
if(s[srt[i]] == '1'){
R = i;
break;
}
for(int i = M ; i ; --i)
if(s[srt[i]] == '0'){
L = i;
break;
}
if(L > R)
puts("0");
else
printf("%d\n" , (val[srt[R]] - val[srt[L]] + MOD) % MOD);
}
return 0;
}

转盘(线段树、单调栈)

毫无疑问要断环成链,那么我们要求的就是\(\min\limits_{i=1}^N\{ \max\limits_{j=0}^{N - 1} \{ a_{j+i} + (N - 1 - j)\} \}\),即\(\min\limits_{i=1}^N \{\max\limits_{j=i}^{N+i-1}\{ a_{j} - j \} + i \} + N - 1\)

所以要算\(\min\limits_{i=1}^N \{\max\limits_{j=i}^{N+i-1}\{ a_{j} - j \} + i \}\)。因为\(a_j - j > a_{j+N} - (j+N)\),所以原式等于\(\min\limits_{i=1}^N \{\max\limits_{j=i}^{2N}\{ a_{j} - j \} + i \}\)。如果\(a_j - j\)有贡献,它一定是后缀最大值,而所有后缀最大值构成的是一个单调栈。对于单调栈中的第\(k\)个元素的位置\(L_k\),我们要求\(\min\limits_{i=L_{k-1} + 1}^N \{ a_{L_k} - L_k + i \}\),显然\(i = L_{k-1}+1\)时有最小值。

所以如果只有\(1\)次询问,可以维护一个单调栈,对于栈内每一个元素找到它之前的最后一个比它大的元素的位置,\(+1\)之后就是它能取到的区间中最左的左端点。

然后考虑修改。不难想到使用线段树维护单调栈(基础实现见楼房重建

对于线段树上每一个点维护单调栈的最大值和答案。线段树上每一个点维护的答案是:对于左儿子传上来的单调栈\(L_1,L_2,...,L_k\),左端点为\(L_1+1,L_2+1,...,L_k+1\)的区间的答案。

每一次合并左右儿子时,因为右半部分一定会是当前单调栈的一部分,所以拿右儿子的最大值\(max\)放在左儿子上二分得到左儿子传上来的单调栈。

如果当前节点的右儿子的最大值\(> max\),意味着当前点维护的单调栈的左儿子传上来的部分一定也是现在正在寻找的栈的一部分,直接把答案拿过来然后递归右儿子,否则递归左儿子。到叶子节点意味着找到了\(L_k\),记得计算一次左端点为\(L_k+1\)时的答案。

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<cctype>
#include<algorithm>
//This code is written by Itst
using namespace std; inline int read(){
int a = 0;
char c = getchar();
while(!isdigit(c))
c = getchar();
while(isdigit(c)){
a = a * 10 + c - 48;
c = getchar();
}
return a;
} const int MAXN = 1e5 + 7;
struct node{
int rgL , rgR , maxN , ans;
}Tree[MAXN << 3];
int val[MAXN << 1] , N , M , P; #define lch (x << 1)
#define rch (x << 1 | 1)
#define mid ((l + r) >> 1)
#define PII pair < int , int > int find(int x , int dir){
if(Tree[x].rgL == Tree[x].rgR)
return Tree[x].rgL + dir + 1;
if(Tree[rch].maxN > dir)
return min(Tree[x].ans , find(rch , dir));
return find(lch , dir);
} inline void pushup(int x){
Tree[x].maxN = max(Tree[rch].maxN , Tree[lch].maxN);
Tree[x].ans = find(lch , Tree[rch].maxN);
} void init(int x , int l , int r){
Tree[x].rgL = l;
Tree[x].rgR = r;
Tree[x].ans = 0x7fffffff;
if(l == r)
Tree[x].maxN = val[l] - l;
else{
init(lch , l , mid);
init(rch , mid + 1 , r);
pushup(x);
}
} void modify(int x , int l , int r , int tar){
if(l == r){
Tree[x].maxN = val[tar] - tar;
return;
}
if(mid >= tar)
modify(lch , l , mid , tar);
else
modify(rch , mid + 1 , r , tar);
pushup(x);
} int main(){
#ifndef ONLINE_JUDGE
freopen("in","r",stdin);
//freopen("out","w",stdout);
#endif
N = read(); M = read(); P = read();
for(int i = 1 ; i <= N ; ++i)
val[i] = val[i + N] = read();
init(1 , 1 , N << 1);
int lastans = Tree[1].ans + N - 1;
cout << lastans << '\n';
for(int i = 1 ; i <= M ; ++i){
int x = P ? read() ^ lastans : read() , y = P ? read() ^ lastans : read();
val[x] = val[x + N] = y;
modify(1 , 1 , N << 1 , x);
modify(1 , 1 , N << 1 , x + N);
cout << (lastans = Tree[1].ans + N - 1) << '\n';
}
return 0;
}

毒瘤(虚树)

如果\(m=n-1\)就是简单的树的独立集个数统计问题,直接树形DP即可;但是相比于树上独立集,这里多了\(11\)条约束。

注意到\(11\)比较小,可以暴力枚举。对于每一条边的两个端点有\((0,1),(0,0),(1,0)\)(\(1\)表示选,\(0\)表示不选)三种情况,而\((0,0),(0,1)\)两种情况可以合成一种,即只确定其中一端不选,另一端不做限制。所以一条边只有两种情况,所以可以\(2^{11}\)地枚举每条边的状态,每一次都在树上DP求一遍独立集个数。

考虑如何优化。注意到每一次只有\(22\)个点的状态会发生变化,故将这\(22\)个点拿出来建立虚树。对于虚树上的每一条边求出其转移系数。求转移系数的方式就是在原树上暴跳父亲,每一次把没有关键点的子树直接拿过来合并。求转移系数对应下面代码里的calc函数部分

#include<iostream>
#include<cstdio>
#include<cctype>
#include<algorithm>
#include<cstring>
#include<iomanip>
#include<vector>
#include<cmath>
//This code is written by Itst
using namespace std; inline int read(){
int a = 0;
char c = getchar();
while(!isdigit(c))
c = getchar();
while(isdigit(c)){
a = a * 10 + c - 48;
c = getchar();
}
return a;
} const int MAXN = 1e5 + 13 , MOD = 998244353;
struct Edge{
int end , upEd;
}Ed[MAXN << 1];
int head[MAXN] , DP[MAXN][2] , dep[MAXN] , dfn[MAXN] , jmp[MAXN][19];
int N , M , cntEd = 1 , cnt , ts , mr[21]; inline void addEd(int a , int b){
Ed[++cntEd].end = b;
Ed[cntEd].upEd = head[a];
head[a] = cntEd;
} #define go(a , b , c) (a = 1ll * a * (b + c) % MOD)
void dfs(int x , int p){
dep[x] = dep[p] + 1;
dfn[x] = ++ts;
jmp[x][0] = p;
for(int i = 1 ; i <= 16 ; ++i)
jmp[x][i] = jmp[jmp[x][i - 1]][i - 1];
DP[x][0] = DP[x][1] = 1;
for(int i = head[x] ; i ; i = Ed[i].upEd)
if(Ed[i].end != p)
if(!dep[Ed[i].end]){
dfs(Ed[i].end , x);
go(DP[x][0] , DP[Ed[i].end][0] , DP[Ed[i].end][1]);
DP[x][1] = 1ll * DP[x][1] * DP[Ed[i].end][0] % MOD;
}
else
if(dep[Ed[i].end] > dep[x])
mr[++cnt] = i;
} bool cmp(int a , int b) {return dfn[a] < dfn[b];} inline int LCA(int x , int y){
if(dep[x] < dep[y])
x ^= y ^= x ^= y;
for(int i = 16 ; i >= 0 ; --i)
if(dep[x] - (1 << i) >= dep[y])
x = jmp[x][i];
if(x == y)
return x;
for(int i = 16 ; i >= 0 ; --i)
if(jmp[x][i] != jmp[y][i]){
x = jmp[x][i];
y = jmp[y][i];
}
return jmp[x][0];
} #define pb push_back
#define add(a , b) (a + b >= MOD ? a + b - MOD : a + b)
vector < int > ch[MAXN] , nd;
int dp[MAXN][2] , xs[MAXN][2][2] , st[101] , top , cntN;
bool mrk[MAXN]; void calc(int x){
xs[x][0][0] = xs[x][1][1] = 1;
for(int i = head[x] ; i ; i = Ed[i].upEd)
if(!mrk[Ed[i].end] && jmp[Ed[i].end][0] == x){
go(xs[x][0][0] , DP[Ed[i].end][1] , DP[Ed[i].end][0]);
xs[x][1][1] = 1ll * xs[x][1][1] * DP[Ed[i].end][0] % MOD;
}
int p = x;
while(!mrk[jmp[p][0]] && jmp[p][0]){
mrk[p = jmp[p][0]] = 1;
int P = xs[x][0][0] , Q = xs[x][0][1];
xs[x][0][0] = add(P , xs[x][1][0]);
xs[x][0][1] = add(Q , xs[x][1][1]);
xs[x][1][0] = P;
xs[x][1][1] = Q;
for(int i = head[p] ; i ; i = Ed[i].upEd)
if(jmp[Ed[i].end][0] == p && !mrk[Ed[i].end]){
go(xs[x][0][0] , DP[Ed[i].end][1] , DP[Ed[i].end][0]);
go(xs[x][0][1] , DP[Ed[i].end][1] , DP[Ed[i].end][0]);
xs[x][1][0] = 1ll * xs[x][1][0] * DP[Ed[i].end][0] % MOD;
xs[x][1][1] = 1ll * xs[x][1][1] * DP[Ed[i].end][0] % MOD;
}
}
} void DFS(int x){
mrk[x] = 1;
for(int i = 0 ; i < ch[x].size() ; ++i)
DFS(ch[x][i]);
calc(x);
} void init(){
nd.pb(1);
for(int i = 1 ; i <= cnt ; ++i){
nd.pb(Ed[mr[i]].end);
nd.pb(Ed[mr[i] ^ 1].end);
}
sort(nd.begin() , nd.end() , cmp);
cntN = unique(nd.begin() , nd.end()) - nd.begin();
for(int i = 0 ; i < cntN ; ++i){
if(top){
int t = LCA(st[top] , nd[i]);
while(top - 1 && dep[t] <= dep[st[top - 1]]){
ch[st[top - 1]].pb(st[top]);
--top;
}
if(dep[t] < dep[st[top]]){
ch[t].pb(st[top]);
st[top] = t;
}
}
st[++top] = nd[i];
}
while(top - 1){
ch[st[top - 1]].pb(st[top]);
--top;
}
DFS(1);
} int sum , zt[MAXN];
void getans(int x){
dp[x][0] = zt[x] == 0 || zt[x] == 1;
dp[x][1] = zt[x] == 0 || zt[x] == 2;
for(int i = 0 ; i < ch[x].size() ; ++i){
getans(ch[x][i]);
go(dp[x][0] , dp[ch[x][i]][0] , dp[ch[x][i]][1]);
dp[x][1] = 1ll * dp[x][1] * dp[ch[x][i]][0] % MOD;
dp[ch[x][i]][0] = dp[ch[x][i]][1] = 0;
}
int p = dp[x][0] , q = dp[x][1];
dp[x][0] = (1ll * xs[x][0][0] * p + 1ll * xs[x][0][1] * q) % MOD;
dp[x][1] = (1ll * xs[x][1][0] * p + 1ll * xs[x][1][1] * q) % MOD;
} void Dfs(int x){
if(x > cnt){
getans(1);
sum = (0ll + sum + dp[1][0] + dp[1][1]) % MOD;
dp[1][0] = dp[1][1] = 0;
return;
}
int l = Ed[mr[x]].end , r = Ed[mr[x] ^ 1].end , p = zt[l] , q = zt[r];
if((p == 0 || p == 1) && (q == 0 || q == 2)){
zt[l] = 1; zt[r] = 2;
Dfs(x + 1);
zt[l] = p; zt[r] = q;
}
if(q == 0 || q == 1){
zt[r] = 1;
Dfs(x + 1);
zt[r] = q;
}
} int main(){
#ifndef ONLINE_JUDGE
freopen("in","r",stdin);
//freopen("out","w",stdout);
#endif
N = read();
M = read();
for(int i = 1 ; i <= M ; ++i){
int a = read() , b = read();
addEd(a , b);
addEd(b , a);
}
dfs(1 , 0);
init();
Dfs(1);
cout << sum;
return 0;
}

游戏(拓扑排序)

不难想到对于一段中间的门都不需要钥匙的区间缩成一个点,然后预处理每一个点能够到达的左右端点

暴力的思路是每一次暴力向左右拓展

但可以知道:对于门\((x,y)\),如果\(y \leq x\),那么一定是从\(x\)所在区间通过这扇门走向\(x+1\)所在区间,反之亦然。那么若左边的某个区间能够通过这扇门,它一定能够到达\(x+1\)所在区间能够到达的所有区间。所以连边\((x,x+1)\)表示要先拓展\(x+1\),后拓展\(x\)。然后拓扑排序决定拓展顺序,一个个拓展就可以过了。复杂度似乎是线性的。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<cctype>
#include<algorithm>
#include<cstring>
#include<iomanip>
#include<queue>
#include<map>
#include<set>
#include<bitset>
#include<stack>
#include<vector>
#include<cmath>
#include<cassert>
//This code is written by Itst
using namespace std; inline int read(){
int a = 0;
char c = getchar();
bool f = 0;
while(!isdigit(c) && c != EOF){
if(c == '-')
f = 1;
c = getchar();
}
if(c == EOF)
exit(0);
while(isdigit(c)){
a = a * 10 + c - 48;
c = getchar();
}
return f ? -a : a;
} const int MAXN = 1e6 + 7;
vector < int > door , ch[MAXN];
int lft[MAXN] , rht[MAXN] , L[MAXN] , R[MAXN] , in[MAXN];
int N , M; inline void add(int x , int y){
++in[y];
ch[x].push_back(y);
} queue < int > q , work;
void TopSort(){
for(int i = 1 ; i <= M ; ++i)
if(!in[i])
q.push(i);
while(!q.empty()){
int t = q.front();
q.pop();
work.push(t);
for(int i = 0 ; i < ch[t].size() ; ++i)
if(!--in[ch[t][i]])
q.push(ch[t][i]);
}
} inline int find(int x){
return lower_bound(door.begin() , door.end() , x) - door.begin();
} inline void calc(int x){
while(L[x] != 1 || door[R[x]] != N){
if(L[x] != 1 && rht[door[L[x] - 1] + 1] && rht[door[L[x] - 1] + 1] <= door[R[x]]){
L[x] = L[L[x] - 1];
continue;
}
if(door[R[x]] != N && lft[door[R[x]]] >= door[L[x] - 1] + 1){
R[x] = R[R[x] + 1];
continue;
}
break;
}
} int main(){
#ifndef ONLINE_JUDGE
freopen("in","r",stdin);
//freopen("out","w",stdout);
#endif
N = read();
M = read();
int Q = read();
for(int i = 1 ; i <= M ; ++i){
int x = read() , y = read();
y <= x ? lft[x] = y : rht[x + 1] = y;
door.push_back(x);
}
++M;
door.push_back(0);
door.push_back(N);
sort(door.begin() , door.end());
for(int i = 1 ; i <= M ; ++i){
L[i] = R[i] = i;
if(rht[door[i - 1] + 1])
add(i - 1 , i);
if(lft[door[i]])
add(i + 1 , i);
}
TopSort();
while(!work.empty()){
calc(work.front());
work.pop();
}
while(Q--){
int l = find(read()) , r = read();
puts(door[L[l] - 1] + 1 <= r && door[R[l]] >= r ? "YES" : "NO");
}
return 0;
}

排列(贪心、并查集)

首先考虑合法排列的限制条件,也就是在排列\(p\)中,对于\(\forall i \in [1,N]\) , \(a_i\)要出现在\(i\)的前面。我们连边\((i,a_i)\),表示\(i\)要在\(a_i\)之后出现。如果存在合法的排列,最后连成的一定是一棵树。

接下来考虑求最大值。有一种比较naive的贪心:每一次都选择当前可以选择的点中最小的。这种贪心策略显然是错的,反例也很好举。

但这似乎能给我们一些启发:对于当前所有点权中最小的数,它一定会在它的父亲被选完之后立即选。那么考虑将它和它的父亲缩成一个点,表示这两个点的选择是连续的。

那么接下来的问题就是缩成的这个点的权值是多少。

不妨设当前所有点权中最小点权对应的点权为\(x\),它的父亲权为\(y\),有另一个点权为\(z\),并且满足\(y\)对应的点和\(z\)对应的点现在都可以选。那么现在有两种决策:1、先选\(y,x\),后选\(z\),权值和为\(y+2x+3z\);2、先选\(z\),后选\(y,x\),权值和为\(z+2y+3x\)

我们需要取更优的,所以只需要比较它们的大小,故同时减去\(x-z\),除以\(2\),那么第一种方案权值为\(\frac{x+y}{2}+2z\),第二种方案权值为\(z + 2\frac{x+y}{2}\)

可以发现之前的两个点\(x,y\)在这个时候可以等价为一个点权为\(\frac{x+y}{2}\)的点。根据这个思路可以得到点权为\(\frac{\text{它所包含的点的点权和}}{\text{它所包含的点的个数}}\)。

那么可以得到一个贪心:每一次选择一个点权最小的点,把它和它父亲合并,可以使用并查集维护合并过程。最后把合并的点拆开计算答案。

#include<bits/stdc++.h>
//This code is written by Itst
using namespace std; inline int read(){
int a = 0;
char c = getchar();
bool f = 0;
while(!isdigit(c)){
if(c == '-')
f = 1;
c = getchar();
}
while(isdigit(c)){
a = (a << 3) + (a << 1) + (c ^ '0');
c = getchar();
}
return f ? -a : a;
} const int MAXN = 5e5 + 10;
struct Edge{
int end , upEd;
}Ed[MAXN << 1];
int head[MAXN] , pre[MAXN << 1] , fa[MAXN << 1] , ch[MAXN << 1][2] , size[MAXN << 1] , cntEd , cntNode , N , t;
long long pri[MAXN << 1] , ans;
bool vis[MAXN] , mark[MAXN << 1];
struct cmp{
bool operator ()(int a , int b){
return pri[a] * size[b] > pri[b] * size[a];
}
};
priority_queue < int , vector < int > , cmp > q; int find(int x){
return fa[x] == x ? x : (fa[x] = find(fa[x]));
} inline void addEd(int a , int b){
Ed[++cntEd].end = b;
Ed[cntEd].upEd = head[a];
head[a] = cntEd;
} bool dfs(int x , int p){
pre[x] = p;
vis[x] = 1;
for(int i = head[x] ; i ; i = Ed[i].upEd)
if(Ed[i].end != p)
if(vis[Ed[i].end] || dfs(Ed[i].end , x))
return 1;
return 0;
} void color(int x){
if(x <= N)
ans += t++ * pri[x];
else{
color(ch[x][0]);
color(ch[x][1]);
}
} int main(){
#ifndef ONLINE_JUDGE
freopen("in" , "r" , stdin);
//freopen("out" , "w" , stdout);
#endif
cntNode = N = read();
size[0] = 1;
for(int i = 1 ; i <= N ; ++i){
int a = read();
addEd(a , i);
addEd(i , a);
fa[i] = i;
size[i] = 1;
}
for(int i = 1 ; i <= N ; ++i){
pri[i] = read();
q.push(i);
}
for(int i = 0 ; i <= N ; ++i)
if(!vis[i])
if(dfs(i , -1)){
puts("-1");
return 0;
}
while(!q.empty()){
int t = q.top();
q.pop();
if(mark[t])
continue;
int f = find(pre[t]) , x = ++cntNode;
fa[f] = fa[t] = fa[x] = x;
size[x] = size[f] + size[t];
pri[x] = pri[f] + pri[t];
ch[x][0] = f;
ch[x][1] = t;
pre[x] = pre[f];
mark[t] = mark[f] = 1;
if(pre[x] != -1)
q.push(x);
}
color(cntNode);
cout << ans;
return 0;
}

道路(树形DP)

设\(f_{i,j,k}\)表示从\(1\)号点到\(i\)号点经过了\(j\)条公路、\(k\)条铁路时,\(i\)及其子树的最小不便利值和,从底往上DP,转移枚举修哪一条边。

#include<bits/stdc++.h>
//This code is written by Itst
using namespace std; inline int read(){
int a = 0;
bool f = 0;
char c = getchar();
while(c != EOF && !isdigit(c)){
if(c == '-')
f = 1;
c = getchar();
}
while(c != EOF && isdigit(c)){
a = (a << 3) + (a << 1) + (c ^ '0');
c = getchar();
}
return f ? -a : a;
} const int MAXN = 20010;
long long dp[1010][41][41] , num[MAXN][3];
int ch[MAXN][2] , headSt , N; void dfs(int now){
++headSt;
if(now < 0){
now = -now;
for(int i = 0 ; i <= 40 ; ++i)
for(int j = 0 ; j <= 40 ; ++j)
dp[headSt][i][j] = (num[now][0] + i) * (num[now][1] + j) * num[now][2];
return;
}
dfs(ch[now][0]);
dfs(ch[now][1]);
for(int i = 0 ; i < 40 ; ++i)
for(int j = 0 ; j < 40 ; ++j)
dp[headSt - 2][i][j] = min(dp[headSt - 1][i + 1][j] + dp[headSt][i][j] , dp[headSt - 1][i][j] + dp[headSt][i][j + 1]);
headSt -= 2;
} int main(){
N = read();
for(int i = 1 ; i < N ; i++){
ch[i][0] = read();
ch[i][1] = read();
}
for(int i = 1 ; i <= N ; i++){
num[i][0] = read();
num[i][1] = read();
num[i][2] = read();
}
dfs(1);
cout << dp[1][0][0];
return 0;
}
上一篇:程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?| 每一张图都力求精美


下一篇:深入理解.NET Core的基元: deps.json, runtimeconfig.json, dll文件