洛谷 P8815 [CSP-J 2022] 逻辑表达式(T3)

目录

题目传送门 

算法解析

中缀转后缀 

后缀转表达式树

对表达式树进行 dfs

最终代码

提交结果

尾声


题目传送门 

[CSP-J 2022] 逻辑表达式 - 洛谷icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P8815

算法解析

首先要说一下这种题的通用解题思路:

  • 中缀表达式转后缀表达式
  • 后缀表达式转表达式树
  • 对表达式树进行 dfs

先说一下什么是中缀表达式、后缀表达式、表达式树

首先,中缀表达式就是我们正常写的表达式,比如

1 & (0 | 1 & 0)

后缀表达式呢,就比如上面那个中缀的后缀就是:

1 0 1 0 & | &

什么意思呢,看一眼下图就明白了

代表他连着的两个蓝线中第一个和第二个用 _ 下边的  连着的运算符运算上,蓝线连着 ∪ 代表那个 ∪ 的运算结果

为什么要把中缀转后缀呢,因为后缀没有括号,这样扫描一遍就能求出值了

表达式树呢,首先它的中序遍历就是中缀表达式,它的后序遍历就是后缀表达式

什么是中序遍历和后序遍历呢,中序就是“左根右”,后序就是“左右根”

比如下面这棵树:

下图为该树的中序遍历(中缀表达式) 

下图为该树的后序遍历(后缀表达式)

为什么要把后缀转表达式树呢,因为这样好判断短路

他一个节点的结果就是它的左子树运算上它的右子树,运算符为它本身

所以只要它本身是 '&' 并且他左子树的结果是 0,那么就是一次 and 的短路

他本身是 '|' 并且他左子树的结果是 1,那么就是一次 or 短路

比如下边的这棵树

 

 他短路后是这样的

中缀转后缀 

那么我们现在就说一下中缀怎么转后缀

  • 首先,要维护一个栈
  • 然后从头到尾遍历整个中缀表达式
  • 如果是数字,直接输出
  • 如果是运算符,弹出栈顶所有优先级大于等于该运算符的运算符,最后将该字符压栈
  • 如果是左括号,压栈
  • 如果是右括号,一直弹栈,直到栈顶为左括号

栈,我们可以用 STL中的 stack

stack <char> st;

然后,我们就需要执行遍历整个中缀表达式了

for(int i = 0; i < infix.size(); ++i)

 然后我们依次判断如果他是什么,就做什么

首先是数字,这里可以用 isdigit,他需要头文件

#include <cctype>

然后看如果他是数字,就直接输出到后缀,然后 continue 掉

这里我后缀用的是 vector 来存储

if(isdigit(infix[i])) {
	suffix.push_back(infix[i]);
	continue;
}

 然后就是看如果他是左括号,压栈,然后 continue 掉

if(infix[i] == '(') {
	st.push(infix[i]);
	continue;
}

 如果他是右括号,就一直弹栈,直到栈顶为左括号,注意,最后也要把左括号弹掉

if(infix[i] == ')') {
	while(st.top() != '(') {
		suffix.push_back(st.top());
		st.pop();
	}
	st.pop();
	
	continue;
}

最后,就剩下运算符了,分别把栈顶所有优先级大于等于它的都弹掉,最后将自己压栈

while(infix[i] == '&' && !st.empty() && st.top() == '&') {
	suffix.push_back(st.top());
	st.pop();
}

while(infix[i] == '|' && !st.empty() && (st.top() == '|' || st.top() == '&')) {
	suffix.push_back(st.top());
	st.pop();
}

st.push(infix[i]);

记得最后要把栈中剩余的全部弹掉:

while(!st.empty()) {
	suffix.push_back(st.top());
	st.pop();
}

我们测试一下:

for(auto p : suffix)
	cout << p << " ";

 这其实就是一种特殊的遍历方法,等于

for(int i = 0; i < suffix.size(); ++i) {
	auto p = suffix[i];
    cout << p << " ";
}

 结果:

OK,对了! 

后缀转表达式树

下一步就是后缀转表达式树了,先说一下怎么转

  • 建立结点结构体
  • 维护一个栈
  • 遍历整个后缀表达式
  • 碰到数字,生成一个值为本身的结点,压栈
  • 碰到运算符,生成一个运算符结点,取出栈顶两个结点,分别为该结点的左孩子和右孩子,压栈
  • 最后栈中肯定剩一个结点,把它设为根结点

好,我们先建立一个结点结构体,可以用指针记录它的左孩子和右孩子

还可以用 op 记录它是否是数字(是数字就是 '$' ,不是数字则是它本身)

还需要记录以它为根结点的树的值,val

可以用一个构造函数,参数为一个字符(数字或运算符),然后看它如果是数字,就把 op 赋成 '$',val 赋成它减 48(变成数字,字符 '0' 的 ASCII 码为 48),否则就是字符,将 op 赋成它本身

struct Node {
	Node *le, *rt;
	char op;
	bool val;
	
	Node(char _op) {
		if(isdigit(_op)) {
			op = '$';
			val = _op - 48;
		} else {
			op = _op;
		}
	}
} *root;

 然后就是维护一个栈

stack <Node*> st;

 现在遍历整个后缀

for(auto ch : suffix)

 然后碰到数字就生成一个值为本身的结点,压栈

if(isdigit(ch))
	st.push(new Node(ch));

否则就生成一个运算符结点,取出栈顶两个结点,分别为该结点的左子树和右子树,压栈

else {
	Node *p = new Node(ch);
	Node *le = st.top();
	st.pop();
	Node *rt = st.top();
	st.pop();
	p->le = le;
	p->rt = rt;
	
	st.push(p);
}

最后就是将根结点设为栈顶结点了

root = st.top();

OK!搞定!\(^o^)/~

(其实这道题如果只要求输出值,就不用建树了,直接遍历后缀表达式,碰见数字就压栈,碰见运算符就将栈顶两个数字弹出,然后用现在的运算符运算上,结果再压栈就可以了,其实和建表达式树的过程是一样的)

对表达式树进行 dfs

最后我们就要开始对表达式树进行 dfs 了

首先要确定参数,可以是当前遍历到的结点

bool dfs(Node *x)

这样,刚开始就要传进去根结点

dfs 可以有一个返回值,记录结果

这样直接输出就可以了

cout << dfs(root) << endl;

先判断如果是数字,直接返回他的值

if(x->op == '$')
    return x->val;

否则的话(运算符),它就返回左孩子运算上右孩子,运算符为它的 op

可以在这个时候记录短路

bool le = dfs(x->le);
if(le == 1 && x->op == '|') {
	++cnt_or;
	return 1;
}
if(le == 0 && x->op == '&') {
	++cnt_and;
	return 0;
}
bool rt = dfs(x->rt);
if(x->op == '|')
	return le || rt;
return le && rt;

最终代码

最终代码如下:

#include <iostream>
#include <string>
#include <vector>
#include <stack>
#include <cctype> 
using namespace std;

struct Node {
	Node *le, *rt;
	char op;
	bool val;
	
	Node(char _op) {
		if(isdigit(_op)) {
			op = '$';
			val = _op - 48;
		} else {
			op = _op;
		}
	}
};

Node *root;
string infix;
vector <char> suffix;
int cnt_and, cnt_or;

void inp() {
	cin >> infix;
}

void build_suffix() {
	stack <char> st;
	for(int i = 0; i < infix.size(); ++i) {
		if(isdigit(infix[i])) {
			suffix.push_back(infix[i]);
			
			continue;
		}
		
		if(infix[i] == '(') {
			st.push(infix[i]);
			
			continue;
		}
		
		if(infix[i] == ')') {
			while(st.top() != '(') {
				suffix.push_back(st.top());
				st.pop();
			}
			st.pop();
			
			continue;
		}
		
		while(infix[i] == '&' && !st.empty() && st.top() == '&') {
			suffix.push_back(st.top());
			st.pop();
		}
		
		while(infix[i] == '|' && !st.empty() && (st.top() == '|' || st.top() == '&')) {
			suffix.push_back(st.top());
			st.pop();
		}
		
		st.push(infix[i]);
	}
	
	while(!st.empty()) {
		suffix.push_back(st.top());
		st.pop();
	}
}

void build_tree() {
	stack <Node*> st;
	for(auto ch : suffix) {
		if(isdigit(ch))
			st.push(new Node(ch));
		else {
			Node *p = new Node(ch);
			Node *rt = st.top();
			st.pop();
			Node *le = st.top();
			st.pop();
			p->le = le;
			p->rt = rt;
			
			st.push(p);
		}
	}
	root = st.top();
}

bool dfs(Node *x) {
	if(x->op == '$')
		return x->val;
	
	bool le = dfs(x->le);
	if(le == 1 && x->op == '|') {
		++cnt_or;
		return 1;
	}
	if(le == 0 && x->op == '&') {
		++cnt_and;
		return 0;
	}
	bool rt = dfs(x->rt);
	if(x->op == '|')
		return le || rt;
	return le && rt;
	
}

void work() {
	build_suffix();
	build_tree();
	cout << dfs(root) << endl;
	cout << cnt_and << " " << cnt_or << endl;
}

int main() {
	inp();
	work();
	
	return 0;
}

提交结果

提交一下~

㇏(〃'▽'〃)㇀ AC ! ! !

尾声

如果这篇博客对您(您的团队)有帮助的话,就帮忙点个赞,加个关注!

最后,祝您(您的团队)在 OI 的路上一路顺风!!!

┬┴┬┴┤・ω・)ノ Bye~Bye~

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: 题目描述: 给定一个逻辑表达式,包含变量和运算符,其中变量用大写字母表示,运算符包括与(&)、或(|)、非(!)和异或(^),请输出该表达式的真值。 输入格式: 输入共一行,为一个字符串,表示逻辑表达式,字符串长度不超过 100。 输出格式: 输出共一行,为一个整数,表示该表达式的真值,1 表示真, 表示假。 输入样例: A&B&C|!D^E 输出样例: 1 解题思路: 本题可以使用来解决,具体思路如下: 1.首先定义两个,一个用来存储运算符,一个用来存储操作数。 2.遍历表达式中的每一个字符,如果是运算符,则将其压入运算符中,如果是操作数,则将其压入操作数中。 3.当遇到一个右括号时,从操作数中弹出两个操作数,从运算符中弹出一个运算符,进行运算,并将结果压入操作数中。 4.最终操作数中只剩下一个元素,即为表达式的真值。 代码实现: ### 回答2: 题目链接:https://www.luogu.com.cn/problem/CSP-J-2022 本题需要我们求得不含变量 `x` 的逻辑表达式的真值表。 首先分析题目中给出的逻辑运算符:`NOT`、`AND`、`OR`、`XOR`。这些逻辑运算符在计算机科学中很常见,它们分别表示取反、且、或、异或的运算。我们可以通过一个简单的真值表来表示它们的运算规则: |p|q|NOT p|p AND q|p OR q|p XOR q| |-|-|-----|------|------|-------| |0|0| 1 | 0 | 0 | 0 | |0|1| 1 | 0 | 1 | 1 | |1|0| 0 | 0 | 1 | 1 | |1|1| 0 | 1 | 1 | 0 | 通过这个真值表,我们可以很容易地推导出不同逻辑表达式的结果。例如,当 `p` 为真且 `q` 为假时,`p AND q` 的结果就是假;`p OR q` 的结果就是真。同理,当 `p` 和 `q` 相同的时候,它们的异或结果就是假,否则就是真。 而本题中的逻辑表达式是由多个运算符和 `x` 变量组成的,我们需要计算出真值表。为了避免直接枚举每一种情况,并逐个计算结果,我们可以采用二叉树的思路,即每个节点代表一个逻辑运算符,左右子树分别为该运算符的两个参数,直到叶节点为止,叶节点即为一个变量(可能是 `x` 或者是常量)。 具体操作方法可以采用递归实现。递归函数需要传入一个字符串表达式,返回该表达式是真还是假。具体实现细节可见代码。最终我们可以得到一个 1 行 2 的 n 次方 列的真值表。 代码: ### 回答3: 题目描述 给定 $m$ 个逻辑变量 $a_1, a_2, \cdots, a_m$,以及 $k$ 个逻辑表达式 $f_1, f_2, \cdots, f_k$,每个逻辑表达式都是由 $m$ 个逻辑变量和 $\land, \lor, \oplus, \neg$ 构成的合法逻辑表达式。 定义 $\operatorname{sat}(S)$ 为集合 $S$ 中的逻辑变量对应的真值赋值下,存在至少一个使得所有的逻辑表达式都为真。 例如,对于逻辑变量 $a, b, c$ 和两个逻辑表达式 $(a \lor b)$ 和 $(b \Rightarrow \neg c)$,$\operatorname{sat}(\{a=\mathrm{true}, b=\mathrm{false}, c=\mathrm{true}\})= false$。 给定一个逻辑表达式 $f$,定义其等效表示为 $\operatorname{simpl}(f)$。具体过程如下: - 根据与运算和或运算的结合律联想律,将逻辑表达式化为形如 $\ell_1 \land \ell_2 \land \cdots \land \ell_p$ 的简单合取范式,其中每个 $\ell_i$ 都是形如 $x_1 \lor x_2 \lor \cdots \lor x_q$ 的简单析取项; - 对于每个简单析取项 $x_1 \lor x_2 \lor \cdots \lor x_q$,在线性时间内计算一个包含了 $S$ 中逻辑变量对应的所有真值赋值中满足其为真的赋值的指示函数 $\phi_{x_1 \lor x_2 \lor \cdots \lor x_q}$。具体地,使用一个变量 $f=\mathrm{false}$ 并进行以下操作 $q$ 次:若 $S$ 中某个逻辑变量对应的值可以使得 $x_i$ 为 true,则让 $f$ 取为 $\mathrm{true}$,最终令 $\phi_{x_1 \lor x_2 \lor \cdots \lor x_q}=f$; - 最终得到的 $\operatorname{simpl}(f)$ 是 $\ell_1', \ell_2', \cdots, \ell_p'$ 的形式,其中每个 $\ell_i'$ 是二元组 $(\phi_{x_{i,1}}, \phi_{x_{i,2}})$,表示存在一组完全赋值 $v$ 使得 $\operatorname{sat}(\{a_i=v_i\})=\mathrm{true}$ 当且仅当 $x_{i,1}(v) \lor x_{i,2}(v)=\mathrm{true}$。 例如,对于逻辑表达式 $(a \land b) \Rightarrow ((a \lor b) \land (\neg a \lor \neg b))$,其 $\operatorname{simpl}$ 值为 $((a, b), ((a,b), (\neg a, \neg b)))$。 现在,请你构造一个逻辑表达式 $g$,使得 $\operatorname{simpl}(g)$ 中只有 $O(k+2^m)$ 个二元组,且对于所有的 $S$,$\operatorname{sat}(S)$ 在 $\operatorname{simpl}(g)$ 中的判定时间复杂度为 $O(k)$。 数据范围 对于所有测试点,保证 $1 \leqslant m \leqslant 10, 1 \leqslant k \leqslant 3m$。 本题满分为 $100$ 分。对于在此公开测试数据中得分为 $0$ 分的提交,我们将根据提交的相似度情况视情况给出提示,请注意查看提交结果页面。 算法1 (打表找规律) $O(m 2^{3m})$ 我们需要解决两个问题:如何根据 $\operatorname{simpl}(f)$ 判断 $\operatorname{sat}(S)$ 是否为 $\mathrm{true}$,以及如何构造出一个表达式 $g$,使得对于所有 $S$ 都有 $\operatorname{simpl}(g)$ 可以在 $O(k)$ 时间内判断 $\operatorname{sat}(S)$ 是否为 $\mathrm{true}$。 先考虑第一个问题。容易发现,对于一个二元组 $(\phi_{x_1}, \phi_{x_2})$,如果 $\operatorname{simpl}(f)$ 中存在一个简单析取项 $x_1 \lor x_2$,且 $\phi_{x_1}$ 和 $\phi_{x_2}$ 均为真函数,则 $\operatorname{simpl}(f)$ 在 $S$ 中的真值即为 $\mathrm{true}$。注意到 $\phi_{x_1}, \phi_{x_2}$ 都是只由 $S$ 中的逻辑变量决定的 01 函数,因此我们可以使用一个 $3^m \times 3^m$ 的矩阵 $mat_x$ 表示 $x$ 对 $S$ 的真值的影响。$mat_x(i,j)=1$ 表示当 $x$ 中逻辑变量取值的指示函数为 $i$ 和 $j$ 时,$\operatorname{sat}(S)$ 是否为 $\mathrm{true}$。注意到 $x$ 有 $2^m$ 种取值,因此我们需要计算 $O(2^m)$ 个矩阵 $mat_x$,每个矩阵的计算可以使用一组对应的二元组 $(\phi_{x_1}, \phi_{x_2})$ 和矩阵加法计算得到。接着我们可以把 $\operatorname{simpl}(f)$ 中所有的简单析取项的对应矩阵加起来,最终得到 $k$ 个 $3^m \times 3^m$ 的矩阵 $mat_1, mat_2, \cdots, mat_k$。判断 $\operatorname{simpl}(f)$ 在 $S$ 中的真值即可以通过计算 $\prod_{i=1}^k mat_i$ 得到。 接着考虑第二个问题。我们需要构造出一个表达式 $g$,使得 $\operatorname{simpl}(g)$ 在 $O(k)$ 时间内能判断 $\operatorname{sat}(S)$ 是否为 $\mathrm{true}$。首先考虑简单情况即 $m=1$。设 $a$ 为 $S$ 中唯一的变量。如果 $k=1$,令 $g=a$ 即可;如果 $k=2$,令 $g=\neg a \land f_2 \lor a\land \neg f_2$ 即可;如果 $k=3$,令 $g=\neg a (\neg f_2 \land \neg f_3) \lor a(\neg f_2 \land f_3) \lor f_2 \land f_3$ 即可;如果 $k=4$,令 $g=a(\neg f_2 \land f_4) \lor \neg a(f_3 \land f_4)$ 即可。可发现,对于给定的 $m,k$,这些表达式都不超过常数个。接着考虑如何推广到一般情况。 设 $a_1, a_2, \cdots, a_m$ 为 $S$ 中的 $m$ 个变量。考虑令 $g_i$ 表示 $S$ 中变量 $a_i$ 的值($0$ 或 $1$)。首先考虑对一个简单析取项 $x_1 \lor x_2 \lor \cdots \lor x_q$ 进行处理。对于 $i \in [1,q]$,设 $x_i$ 的指示函数为 $(\phi_{x_i,1}, \phi_{x_i,2})$,考虑令 $y_i$ 表示“至少有一个 $x_1, x_2, \cdots, x_i$ 在 $S$ 中是真的”,即 $y_1=x_1, y_i=y_{i-1} \lor x_i$。注意到 $y_i$ 极易通过 $\mathrm{or}$ 的结合律计算得到,因此我们可以直接把所有的二元组加入表达式 $g$ 中。此外,为了判断 $\operatorname{sat}(S)$ 是否为 $\mathrm{true}$,我们只需要计算 $y_q$ 即可。 接着考虑如何计算 $g_i$。我们构建一个由 $2^m$ 个节点组成的满二叉树,每个节点代表一个赋值 $v_1, v_2, \cdots, v_m$。根节点对应所有变量的值都是未定的赋值。为了方便起见,我们设根节点所在的深度为 $1$(节点深度定义为从根节点到该节点的从上到下路径上穿过的边数目加一)。除了根节点外,每个节点 $u$ 都对应一个深度为 $i_u$ 的变量 $a_{u, i_u}$ 的值。仅当 $a_{u, i_u}$ 的值已经被确定时,才可以计算 $g_{i_u}(u)$(为了防止重复计算,不考虑已经被计算过的 $g$ 值)。$g_{i_u}(u)$ 的值也是二元组 $(\phi_{x_{i_u,1}}, \phi_{x_{i_u,2}})$ 的形式,表示当前 $\operatorname{sat}(S)$ 在 $u$ 对应的赋值下为真的充要条件为 $\phi_{x_{i_u,1}}(u)=\mathrm{true}$ 或 $\phi_{x_{i_u,2}}(u)=\mathrm{true}$。这些值可以通过矩阵乘法计算得到(注意到构成 $g_{i_u}(u)$ 的所有简单析取项的矩阵已经计算得到)。最后根据 $g_1(\mathrm{root}) \lor g_2(\mathrm{root}) \lor \cdots \lor g_m(\mathrm{root})$ 判断 $\operatorname{sat}(S)$ 是否为 $\mathrm{true}$ 即可。 时间复杂度 计算 $\operatorname{simpl}(f)$ 中所有 二元组需 $O(k \cdot 2^m \cdot 3^{2m})$ 的时间,处理 $g$ 的过程中遍历了一个深度为 $m+1$ 的二叉树,节点数为 $2^{m+1}-1$。每个节点计算 $g_{i_u}(u)$ 的时间复杂度为 $O(2^{3m})$,因此总时间复杂度为 $O(k \cdot 2^m \cdot 3^{2m} + (2^{m+1}-1)2^{3m}) \leqslant O(m 2^{3m})$。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值