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\)
那麽最後的答案就會表現為\(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; }
轉盤(線段樹、單調棧)
很久以前對著yyb的題解做的
所以忘了……回憶一下再來補上
毒瘤(虛樹)
如果\(m=n-1\)就是簡單的樹的獨立集個數統計問題,直接樹形DP即可;但是相比於樹上獨立集,這裏多了\(11\)條約束。
註意到\(11\)比較小,可以暴力枚舉。對於每一條邊的兩個端點有\((0,1),(0,0),(1,0)\)(\(1\)表示選,\(0\)表示不選)三種情況,而\((0,0),(0,1)\)兩種情況可以合成一種,即只確定其中一端不選,另一端不做限制。所以一條邊只有兩種情況,所以可以\(2^{11}\)地枚舉每條邊的狀態,每一次都在樹上DP求一遍獨立集個數。
考慮如何優化。註意到每一次只有\(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;
}
HNOI2018做題筆記