【题解】 「WC2021」表达式求值 按位+表达式树+树形dp LOJ3463
Legend
Link \(\textrm{to LOJ}\)。
Editorial
想想不含有 ?
的怎么做?考虑数组的每一位是独立的,不妨一位一位解决。我们只要求每一位最终是哪一个数字最后对答案造成了贡献。
不难想到二分答案,把 \(\ge mid\) 的值设置成 \(1\),\(< mid\) 的值设置成 \(0\),取 \(\max\) 就变成了或,取 \(\min\) 就变成了与。
在表达式树上算出答案的最终结果,如果是 \(1\),就说明答案比当前二分值大了或者恰好,否则说明答案比当前二分值小了。
但是由于 \(m=10\) 我们没有必要二分,直接对每一个情况算出了都是可以的。
但是对于 \(n=50000\) 个位分开做这个事情就太慢了,于是我就用 bitset 解决了这个问题。
(显然就是这一步把我从前往正解的路上带歪了)
但是你发现虽然 \(n=50000\),但本质不同的 01 大小关系只有 \(2^m\) 个,于是直接算这 \(2^m\) 个就行了。
囧……
推广到正解就在表达式树上 dp 出方案就行了。
复杂度 \(O(2^m|E|+nm^2)\)。
Code
#include <bits/stdc++.h>
#define debug(...) fprintf(stderr ,__VA_ARGS__)
#define __FILE(x)\
freopen(#x".in" ,"r" ,stdin);\
freopen(#x".out" ,"w" ,stdout)
#define LL long long
const int MX = 1e5 + 23;
const LL MOD = 1e9 + 7;
using namespace std;
int read(){
char k = getchar(); int x = 0;
while(k < '0' || k > '9') k = getchar();
while(k >= '0' && k <= '9') x = x * 10 + k - '0' ,k = getchar();
return x;
}
int n ,m;
int A[10][MX];
char S[MX]; int slen;
char suf[MX];
char stk[MX];
int cnt ,top;
int ch[MX][2] ,val[MX] ,tot ,rt;
bitset<50000> AAA[10] ,ans[10];
int qwq ,fa[MX];
void init(){for(int i = 0 ; i < MX ; ++i) fa[i] = i;}
int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
int getid(char k){
if(k == '>') return 1;
if(k == '<') return 0;
return 2;
}
int Build(){
stack<int> tmp;
int sz = 0;
for(int i = 1 ; i <= cnt ; ++i){
if(isdigit(suf[i])){
val[++sz] = suf[i] - '0';
tmp.push(sz);
}
else{
val[++sz] = getid(suf[i]);
ch[sz][1] = find(tmp.top());
fa[find(tmp.top())] = sz; tmp.pop();
ch[sz][0] = find(tmp.top());
fa[find(tmp.top())] = sz; tmp.pop();
tmp.push(sz);
}
}
return sz;
}
#define lc ch[x][0]
#define rc ch[x][1]
namespace STD{
int dp[MX][2] ,ans[1 << 10];
void dapai(int x ,int Z){
if(!ch[x][0]){
dp[x][1] = (Z >> val[x]) & 1;
dp[x][0] = !dp[x][1];
return;
}
dapai(ch[x][0] ,Z);
dapai(ch[x][1] ,Z);
if(val[x] == 0){ // & op
dp[x][1] = 1LL * dp[lc][1] * dp[rc][1] % MOD;
dp[x][0] = (1LL * (dp[lc][1] + dp[lc][0])
* (dp[rc][1] + dp[rc][0]) - dp[x][1] + MOD) % MOD;
}
if(val[x] == 1){
dp[x][0] = 1LL * dp[lc][0] * dp[rc][0] % MOD;
dp[x][1] = (1LL * (dp[lc][1] + dp[lc][0])
* (dp[rc][1] + dp[rc][0]) - dp[x][0] + MOD) % MOD;
}
if(val[x] == 2){
dp[x][0] = (1LL * dp[lc][0] * dp[rc][0]
+ 1LL * dp[lc][0] * dp[rc][1]
+ 1LL * dp[lc][1] * dp[rc][0] /* AND */
+ 1LL * dp[lc][0] * dp[rc][0]) % MOD; /* OR */
dp[x][1] = (1LL * dp[lc][1] * dp[rc][1] /* AND */
+ 1LL * dp[lc][1] * dp[rc][1] /* OR */
+ 1LL * dp[lc][1] * dp[rc][0]
+ 1LL * dp[lc][0] * dp[rc][1]) % MOD;
/*dp[x][0] %= MOD;
dp[x][1] %= MOD;
*/
}
}
void Main(){
init();
rt = Build();
for(int i = 0 ; i < (1 << m) ; ++i){
dapai(rt ,i);
ans[i] = dp[rt][1];
}
LL ret = 0;
for(int i = 0 ; i < n ; ++i){
int tmp[10];
for(int j = 0 ; j < m ; ++j){
tmp[j] = A[j][i];
}
std::sort(tmp ,tmp + m);
int las = 0;
for(int j = m - 1 ; ~j ; --j){ // 枚举排名 >= 的都变成 1
int st = 0;
for(int k = 0 ; k < m ; ++k){
st |= (A[k][i] >= tmp[j]) << k;
}
ret = (ret + 1LL * tmp[j] * (ans[st] - las + MOD)) % MOD;
las = ans[st];
}
}
printf("%lld\n" ,ret);
}
}
int main(){
// __FILE(expr);
n = read() ,m = read();
for(int i = 0 ; i < m ; ++i){
for(int j = 0 ; j < n ; ++j){
A[i][j] = read();
}
}
scanf("%s" ,S); slen = strlen(S);
int ques = 0;
for(int i = 0 ; i < slen ; ++i){
if(isdigit(S[i])){
suf[++cnt] = S[i];
if(top && stk[top] != '(') suf[++cnt] = stk[top--];
}
else{
if(S[i] == '?') ques = 1;
if(S[i] == ')'){
--top;
if(top && stk[top] != '('){
suf[++cnt] = stk[top--];
}
}
else stk[++top] = S[i];
}
}
STD::Main();
return 0;
}
Note
贴一下我考试的时候写的速记,打了 85 分的部分分。但最后因为 \(n=2\) 的时候树上 dp,把 long long 的结果赋值给了 int,直接暴毙 20 分。
T2 不含问号的做法:
把表达式树建立出来。
每一位独立考虑:
显然 10 个数字可以按大小排序,二分一下最终答案是谁,小于的设置成 0,大于等于的设置成 1。
这样子取 max 操作也就变成了按位或,取 min 操作就是按位与。
变成了按位操作之后就发现可以把位合并了,把二分换成暴力枚举,用 bitset 加速可以做到 O(mn|E| / w)
如果 i -> i - 1 时的答案从 0 变成了 1,则该位最终答案就是排名第 i - 1 的那个数。
这样子就可以对每一位算出是谁造成的贡献了。
至少 50 分是可以拿到的。
------------------
n=2 且没有括号:
因为 n 小,所以可以直接对每一位算贡献,算出答案 >= k 的方案,然后通过差分得到答案 =k 的方案。
不难发现可以用寻宝游戏的那个套路,把数字串从右到左看成是个二进制数;
把操作符串也看成二进制数 |->0 &->1,则操作符串严格小于数字串时最终结果是 1。
等于说给定两个二进制数 A,B。B 有一些位置不确定,问多少种使得 B<A 的方案。
显然,直接枚举第一个二进制位小于的地方,然后后面随便放。
因为 n=2 所以 复杂度依然正确。
------------------
但实际上,感觉上文那个做法有点小题大做的感觉。
你发现只要一遍表达式树上的 dp 就可以算出根节点是 1 的方案。
这玩意复杂度是 O(nm|E|) 的