CSP歷年複賽題-P1310 [NOIP2011 普及組] 表示式的值

江城伍月發表於2024-05-30

原題連結:https://www.luogu.com.cn/problem/P1310

題意解讀:+代表按位或運算,*代表按位與運算,給定一個沒有填數字的表示式,要求結果為0的數字方案數。

解題思路:

下面一步一步,由淺入深的來解決本題

思路一(20分做法):

觀察得知,20%的資料,只有10個符號,且沒有括號,也就是對應數字最多11個,可以考慮DFS列舉所有長度是n+1的01數字組合,然後根據運算子、數字進行表示式計算。

需要注意的點是,對於中序表示式,在求值的時候,需要使用兩個棧:運算子棧、運算元棧,主要流程如下:

1、遍歷運算子和運算元

2、如果運算子棧為空,則將運算子和對應兩個運算元入棧

3、如果運算子棧不為空,且當前運算子優先順序不高於棧頂運算子的優先順序,則將棧頂運算子和對應的兩個運算元彈出進行運算,結果入運算元棧

4、再把當前運算子和下一個運算元分別入棧

5、最後,對棧中剩餘的運算子和運算元進行一遍運算

6、結果即運算元棧頂元素

20分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int MOD = 10007;
int n;
string str;
int a[15]; //n+1個0/1數字的組合
int ans = 0;

map<char, int> priority = {{'+', 1} , {'*', 2}};

int eval()
{
    stack<char> op; //運算子棧
    stack<int> num; //數字棧
    for(int i = 0; i < str.size(); i++) 
    {
        if(op.empty()) //運算子棧為空,則將運算子,左右運算元入棧
        {
            op.push(str[i]);
            num.push(a[i]);
            num.push(a[i + 1]);
            continue;
        }
        //如果運算子棧不空,且當前運算子優先順序不高於棧頂運算子優先順序
        //則彈出棧頂運算子和相應數字,進行計算,之後再把當前運算子和右運算元入棧
        if(op.size() && priority[str[i]] <= priority[op.top()]) 
        {
            char c = op.top(); op.pop();
            int a = num.top(); num.pop();
            int b = num.top(); num.pop();
            int res;
            if(c == '*') res = a & b;
            else res = a | b;
            num.push(res);
        }
        op.push(str[i]);
        num.push(a[i + 1]);
    }
    //最後對棧中剩餘運算子和運算元進行計算
    while(op.size())
    {
            char c = op.top(); op.pop();
            int a = num.top(); num.pop();
            int b = num.top(); num.pop();
            int res;
            if(c == '*') res = a & b;
            else res = a | b;
            num.push(res);
    }
    //最終結果為棧頂數字
    return num.top();
}   

void dfs(int k)
{
    if(k == n + 1) //列舉到一組n + 1個0、1的數的組合
    {
        if(eval() == 0) ans++; //計算表示式的值
        return;
    }
    a[k] = 0;
    dfs(k + 1);

    a[k] = 1;
    dfs(k + 1);
}

int main()
{
    cin >> n >> str;
    dfs(0);
    cout << ans % MOD;
    return 0;
}

思路2(50分做法):

由於前50%的資料長度不超過1000,且沒有括號,可以利用組合數的思想:

對於一個+*組成的表示式,要為0,必須+對應的所有運算元都為0

表示式中可能有多個連續的*,形如+*+**+,完整表示式為_+_*_+_*_*_+_

結果為0,則有_ , _*_, _*_*_, _這四個都是0

_,代表一個數,為0的方案數只有1種

_*_,代表兩個數做與運算,為0的方案有01,10,00 3種,也就是2個數,其中有可能有1個0,2個0,對應的方案數為C(2,1)+C(2,2) = 3種

_*_*_,代表三個數做與運算,為0的方案有C(3,1)+C(3,2)+C(3,3) = 7種

_,代表一個數,為0的方案只有1種

所以,_+_*_+_*_*_+_表示式為0的方案數為1 * 3 * 7 * 1 = 21種

基於以上分析,只需要找到表示式中的連續的*,每cnt個*,代表有cnt+1個數做與運算,為0的方案是C(cnt+1,1)+C(cnt+1,2)+...+C(cnt+1,cnt+1)

所有連續*的方案數相乘即可。

注意需要對組合數進行預處理。

50分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int MOD = 10007;
int n;
string str;
int ans = 1;

int c[1005][1005];

int main()
{
    cin >> n >> str;

    //初始化組合數
    for(int i = 0; i <= 1000; i++)
    {
        for(int j = 0; j <= i; j++)
        {
            if(j == 0) c[i][j] = 1;
            else c[i][j] = (c[i-1][j-1] + c[i-1][j]) % MOD;
        }
    }

    int cnt = 0; 
    for(int i = 0; i < str.size(); i++)
    {
        if(str[i] == '*') cnt++;
        else
        {
            //處理連續cnt個*
            if(cnt > 0)
            {
                int sum = 0; //cnt個*對應的表示式計算為0的方案數,也就是cnt+1個數有幾個1~cnt+1個0的方案數:c(cnt,0)+c(cnt,1)+...+c(cnt,cnt)種
                for(int j = 1; j <= cnt + 1; j++)
                {
                    sum += c[cnt+1][j]; sum %= MOD;
                }   
                ans *= sum; ans %= MOD;
                cnt = 0;
            }
        }
    }
    if(cnt > 0)
    {
        int sum = 0; //cnt個*對應的表示式計算為0的方案數,也就是cnt+1個數有幾個1~cnt+1個0的方案數:c(cnt,0)+c(cnt,1)+...+c(cnt,cnt)種
        for(int j = 1; j <= cnt + 1; j++)
        {
            sum += c[cnt+1][j]; sum %= MOD;
        }   
        ans *= sum; ans %= MOD;
        cnt = 0;
    }
    cout << ans;

    return 0;
}

思路3(100分做法):

如果知道了中綴表示式的計算方法,也知道了此題跟組合數的關係,更進一步可以得出:

如果要計算結果是0的方案數

a+b是0的方案數 = a是0的方案數 * b是0的方案數

a*b是0的方案數 = a是0的方案數 * b是0的方案數 + a是0的方案數 * b是1的方案數 + a是1的方案數 * b是0的方案數

因此,我們可以藉助表示式求值的程式碼流程,但實際進行計算是,不是具體計算兩個數的+或者*,而是遞推的計算

{結果是0的方案數、結果是1的方案數},

運算元棧存的就是一個結構體

struct node 
{
    int cnt0; //結果為0的方案數
    int cnt1; //結果為1的方案數
};

100分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int MOD = 10007;
int n;
string str;

struct node
{
    int cnt0; //結果為0的方案數
    int cnt1; //結果為1的方案數
};

stack<char> op;
stack<node> num;

map<char, int> priority = {{'+', 1} , {'*', 2}}; //運算子的優先順序


void eval()
{
    char c = op.top(); op.pop();
    node a = num.top(); num.pop();
    node b = num.top(); num.pop();
    node res;
    if(c == '+')
    {
        //兩邊都是0,+才是0
        res.cnt0 = (a.cnt0 * b.cnt0) % MOD; 
        //有一邊是1,+就是1
        res.cnt1 = (a.cnt0 * b.cnt1 + a.cnt1 * b.cnt0 + a.cnt1 * b.cnt1) % MOD; 
    } 
    else if(c == '*')
    {
        //有一邊是0,*就是0
        res.cnt0 = (a.cnt0 * b.cnt1 + a.cnt1 * b.cnt0 + a.cnt0 * b.cnt0) % MOD;
        //兩邊都是1,*才是1
        res.cnt1 = (a.cnt1 * b.cnt1) % MOD;
    }
    num.push(res);
}

int main()
{
    cin >> n >> str;

    num.push({1, 1}); //第一個運算元,只有1位,結果為0和為1的方案數都是1
    for(int i = 0; i < str.size(); i++)
    {
        if(str[i] == '(') op.push(str[i]);
        else if(str[i] == ')')
        {
            while(op.top() != '(') eval();
            op.pop(); //彈出')'
        }
        else 
        {
            while(op.size() && op.top() != '(' && priority[str[i]] <= priority[op.top()])
                eval();
            op.push(str[i]); //存入當前運算子
            num.push({1, 1}); //存入右運算元
        }
    }
    while(op.size()) eval(); //處理棧中剩餘運算子

    cout << num.top().cnt0; //資料棧的結果0的方案數即答案

    return 0;
}

相關文章