#P2781. 第2题-文本数据
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 46
            Accepted: 4
            Difficulty: 8
            
          
          
          
                       所属公司 : 
                              阿里
                                
            
                        
              时间 :2025年3月30日-阿里云(算法岗)
                              
                      
          
 
- 
                        算法标签>模拟          
 
第2题-文本数据
题解
题面描述
给定包含 N 篇文档的文本数据集,每篇文档由一个类别标签(取值为 "positive" 或 "negative")和文本内容构成。要求使用 Bag-of-Words 模型提取文本特征,利用卡方检验计算每个单词与类别标签之间的相关性。卡方统计量的计算公式为
χ2= ∑i=12 ∑j=12 Eij(Oij−Eij)2
其中 Oij 表示观察到的频数,Eij 表示在独立性假设下计算的期望频数,计算公式为
具体步骤如下:
- 读取输入数据集,第一行是文档数 N,接下来的 N 行每行格式为
<label>\t<text>
最后一行输入整数 k 表示需要选择的特征个数。 - 对每篇文档使用 Bag-of-Words 模型提取单词(注意区分大小写),统计每个单词在不同类别下出现的文档数(每篇文档中对某单词只计一次)。
 - 对于每个单词,构造 2×2 的列联表:
- A:正类中出现该单词的文档数
 - B:负类中出现该单词的文档数
 - C:正类中未出现该单词的文档数 = npositive−A
 - D:负类中未出现该单词的文档数 = nnegative−B
 
 - 计算每个单词的卡方统计量。例如,计算单元格 (1,1) 的期望频数为E11=N(A+B)×npositive. 对 4 个单元格均计算 Eij(Oij−Eij)2 后求和即为该单词的卡方值。
 - 按照卡方统计量从大到小排序,若相同则按单词字典序升序。输出前 k 个单词作为选定的特征。
 - 特殊情况:若输入格式不正确(例如实际文档数与 N 不符)则输出 
-1。 
思路与代码分析
- 数据读取与预处理
首先读取第一行的整数 N。检查后续行数是否满足 N+1 行(N 行数据 + 1 行 k 值),否则直接输出-1。 - 特征提取
对每个文档,根据制表符拆分出类别和文本,然后使用空格分割文本得到单词列表。对每个文档使用集合保证同一单词在文档中只统计一次。 - 统计频数
分别统计正类和负类的文档数,并构造每个单词的频数统计,即 A 和 B。 - 卡方统计量计算
根据 A、B、C=npositive−A、D=nnegative−B,以及总文档数 N 计算期望值 Eij,再累加得到卡方值。 - 排序与输出
将所有单词按照卡方值降序排序,若卡方值相同则按单词的字母序升序,最后输出前 k 个特征。 
c++
#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <cmath>
#include <algorithm>
using namespace std;
// 定义结构体保存单词及其卡方值
struct Feature {
    string word;
    double chi;
};
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int N;
    if(!(cin >> N)){
        cout << -1;
        return 0;
    }
    cin.ignore(); // 忽略换行
    vector<string> docs;
    // 读取 N 行文档
    for (int i = 0; i < N; i++){
        string line;
        if(!getline(cin, line)){
            cout << -1;
            return 0;
        }
        docs.push_back(line);
    }
    // 读取最后一行的 k 值
    int k;
    if(!(cin >> k)){
        cout << -1;
        return 0;
    }
    
    int positiveCount = 0, negativeCount = 0;
    // 保存每个单词在正负类文档中出现的次数
    unordered_map<string, pair<int, int>> wordCount;
    for(auto &line : docs){
        string label, text;
        size_t pos = line.find('\t');
        if(pos != string::npos){
            // 如果存在制表符,则按照制表符拆分
            label = line.substr(0, pos);
            text = line.substr(pos+1);
        } else {
            // 否则按空格拆分,第一个单词为标签,其余为文本
            istringstream iss(line);
            if(!(iss >> label)){
                cout << -1;
                return 0;
            }
            getline(iss, text);
            // 去掉 text 开头的空格
            if(!text.empty() && text[0]==' ') text.erase(0, 1);
        }
        
        bool isPositive = false;
        if(label == "positive"){
            positiveCount++;
            isPositive = true;
        } else if(label == "negative"){
            negativeCount++;
        } else {
            cout << -1;
            return 0;
        }
        
        // 使用 set 保证同一文档中每个单词只计一次
        unordered_set<string> wordsInDoc;
        istringstream iss(text);
        string word;
        while(iss >> word){
            wordsInDoc.insert(word);
        }
        for(auto &w : wordsInDoc){
            if(isPositive)
                wordCount[w].first++;
            else
                wordCount[w].second++;
        }
    }
    
    int total = positiveCount + negativeCount;
    vector<Feature> features;
   
    for(auto &p : wordCount){
        string word = p.first;
        int A = p.second.first;  // 正类中出现该单词
        int B = p.second.second; // 负类中出现该单词
        int C = positiveCount - A; // 正类中未出现该单词
        int D = negativeCount - B; // 负类中未出现该单词
        
        double chi = 0.0;
        int row1 = A + B; // 单词出现的总文档数
        int row2 = C + D; // 单词未出现的文档数
        int col1 = positiveCount;
        int col2 = negativeCount;
        
        // 对4个单元格分别计算贡献,注意避免除以 0
        if(row1 > 0 && col1 > 0) {
            double E = double(row1) * col1 / total;
            if(E > 0) chi += (A - E) * (A - E) / E;
        }
        if(row1 > 0 && col2 > 0) {
            double E = double(row1) * col2 / total;
            if(E > 0) chi += (B - E) * (B - E) / E;
        }
        if(row2 > 0 && col1 > 0) {
            double E = double(row2) * col1 / total;
            if(E > 0) chi += (C - E) * (C - E) / E;
        }
        if(row2 > 0 && col2 > 0) {
            double E = double(row2) * col2 / total;
            if(E > 0) chi += (D - E) * (D - E) / E;
        }
        
        features.push_back({word, chi});
    }
    
    // 按照卡方值从大到小排序,若相同则按字母序升序
    sort(features.begin(), features.end(), [](const Feature &a, const Feature &b) {
        if(fabs(a.chi - b.chi) > 1e-9)
            return a.chi > b.chi;
        return a.word < b.word;
    });
    
    // 输出前k个特征
    for (int i = 0; i < k && i < features.size(); i++){
        cout << features[i].word << "\n";
    }
    
    return 0;
}
python
# -*- coding: utf-8 -*-
import sys
import math
def main():
    # 读取所有输入行
    lines = sys.stdin.read().splitlines()
    if not lines:
        print(-1)
        return
    try:
        # 第一行为文档数量N
        N = int(lines[0])
    except:
        print(-1)
        return
    # 检查是否有足够的行:N行文档 + 1 行k
    if len(lines) != N + 2:
        print(-1)
        return
    positive_count = 0
    negative_count = 0
    word_count = {}  # 字典,键为单词,值为 [正类出现次数, 负类出现次数]
    # 遍历每篇文档
    for i in range(1, N+1):
        line = lines[i]
        # 如果存在制表符,则用制表符分割;否则按空格拆分,第一个单词为标签
        if "\t" in line:
            label, text = line.split("\t", 1)
        else:
            parts = line.split()
            if len(parts) < 2:
                print(-1)
                return
            label = parts[0]
            text = " ".join(parts[1:])
            
        if label == "positive":
            positive_count += 1
            is_positive = True
        elif label == "negative":
            negative_count += 1
            is_positive = False
        else:
            print(-1)
            return
        
        # 用集合保证每篇文档中每个单词只计一次
        words = set(text.split())
        for word in words:
            if word not in word_count:
                word_count[word] = [0, 0]
            if is_positive:
                word_count[word][0] += 1
            else:
                word_count[word][1] += 1
    total = positive_count + negative_count
    features = []
    # 计算每个单词的卡方值
    for word, counts in word_count.items():
        A = counts[0]  # 正类中出现该单词
        B = counts[1]  # 负类中出现该单词
        C = positive_count - A  # 正类中未出现该单词
        D = negative_count - B  # 负类中未出现该单词
        row1 = A + B  # 单词出现的总文档数
        row2 = C + D  # 单词未出现的文档数
        col1 = positive_count
        col2 = negative_count
        
        chi = 0.0
        # 计算每个单元格的期望频数E并累加贡献
        if row1 > 0 and col1 > 0:
            E = row1 * col1 / total
            if E > 0:
                chi += (A - E) ** 2 / E
        if row1 > 0 and col2 > 0:
            E = row1 * col2 / total
            if E > 0:
                chi += (B - E) ** 2 / E
        if row2 > 0 and col1 > 0:
            E = row2 * col1 / total
            if E > 0:
                chi += (C - E) ** 2 / E
        if row2 > 0 and col2 > 0:
            E = row2 * col2 / total
            if E > 0:
                chi += (D - E) ** 2 / E
        features.append((word, chi))
    # 按照卡方值从大到小排序,若卡方值相同则按字母序升序
    features.sort(key=lambda x: (-x[1], x[0]))
    
    try:
        k = int(lines[-1])
    except:
        print(-1)
        return
    # 输出前k个特征
    for i in range(min(k, len(features))):
        print(features[i][0])
        
if __name__ == "__main__":
    main()
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
// 定义 Feature 类保存单词及其卡方值
class Feature {
    String word;
    double chi;
    Feature(String word, double chi) {
        this.word = word;
        this.chi = chi;
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = br.readLine();
        if(line == null){
            System.out.println(-1);
            return;
        }
        int N;
        try {
            // 读取文档数量N
            N = Integer.parseInt(line.trim());
        } catch(Exception e) {
            System.out.println(-1);
            return;
        }
        
        List<String> docs = new ArrayList<>();
        // 读取N行文档
        for(int i = 0; i < N; i++){
            String docLine = br.readLine();
            if(docLine == null){
                System.out.println(-1);
                return;
            }
            docs.add(docLine);
        }
        // 读取最后一行的k值
        String kLine = br.readLine();
        if(kLine == null){
            System.out.println(-1);
            return;
        }
        int k;
        try {
            k = Integer.parseInt(kLine.trim());
        } catch(Exception e) {
            System.out.println(-1);
            return;
        }
        
        int positiveCount = 0;
        int negativeCount = 0;
        // 使用 Map 保存每个单词在正负类中的出现次数,值为 int 数组:[正类出现次数, 负类出现次数]
        Map<String, int[]> wordCount = new HashMap<>();
        
        for(String doc : docs){
            String label, text;
            // 如果包含制表符,则按制表符拆分;否则按空格拆分,第一个单词为标签,其余为文本
            if(doc.contains("\t")){
                String[] parts = doc.split("\t", 2);
                if(parts.length < 2){
                    System.out.println(-1);
                    return;
                }
                label = parts[0];
                text = parts[1];
            } else {
                String[] parts = doc.split(" ");
                if(parts.length < 2){
                    System.out.println(-1);
                    return;
                }
                label = parts[0];
                text = String.join(" ", Arrays.copyOfRange(parts, 1, parts.length));
            }
            
            boolean isPositive;
            if(label.equals("positive")){
                positiveCount++;
                isPositive = true;
            } else if(label.equals("negative")){
                negativeCount++;
                isPositive = false;
            } else {
                System.out.println(-1);
                return;
            }
            
            // 使用 Set 保证同一文档中每个单词只计一次
            String[] words = text.split(" ");
            Set<String> wordSet = new HashSet<>(Arrays.asList(words));
            for(String word : wordSet){
                int[] counts = wordCount.getOrDefault(word, new int[2]);
                if(isPositive)
                    counts[0]++;
                else
                    counts[1]++;
                wordCount.put(word, counts);
            }
        }
        
        int total = positiveCount + negativeCount;
        List<Feature> features = new ArrayList<>();
        // 遍历每个单词计算卡方值
        for(Map.Entry<String, int[]> entry : wordCount.entrySet()){
            String word = entry.getKey();
            int A = entry.getValue()[0]; // 正类中出现该单词
            int B = entry.getValue()[1]; // 负类中出现该单词
            int C = positiveCount - A;   // 正类中未出现该单词
            int D = negativeCount - B;   // 负类中未出现该单词
            
            int row1 = A + B; // 单词出现的文档总数
            int row2 = C + D; // 单词未出现的文档数
            int col1 = positiveCount;
            int col2 = negativeCount;
            
            double chi = 0.0;
            // 对每个单元格计算期望频数E并累加贡献
            if(row1 > 0 && col1 > 0){
                double E = (double)row1 * col1 / total;
                if(E > 0)
                    chi += (A - E) * (A - E) / E;
            }
            if(row1 > 0 && col2 > 0){
                double E = (double)row1 * col2 / total;
                if(E > 0)
                    chi += (B - E) * (B - E) / E;
            }
            if(row2 > 0 && col1 > 0){
                double E = (double)row2 * col1 / total;
                if(E > 0)
                    chi += (C - E) * (C - E) / E;
            }
            if(row2 > 0 && col2 > 0){
                double E = (double)row2 * col2 / total;
                if(E > 0)
                    chi += (D - E) * (D - E) / E;
            }
            
            features.add(new Feature(word, chi));
        }
        
        // 按照卡方值降序排序,若相同则按字母序升序
        Collections.sort(features, new Comparator<Feature>(){
            public int compare(Feature f1, Feature f2){
                if(Math.abs(f1.chi - f2.chi) > 1e-9){
                    return f1.chi > f2.chi ? -1 : 1;
                }
                return f1.word.compareTo(f2.word);
            }
        });
        
        // 输出前k个特征
        for(int i = 0; i < k && i < features.size(); i++){
            System.out.println(features.get(i).word);
        }
    }
}
        题目内容
假设你团队正在开发一个文本分类模型,用于将客户评论分类为正面或负面。由于文本数据具有高维度的特性,模型训练和预测的效率受到影响。你提议使用卡方检验进行特征选择,挑选出与分类任务最相关的词汇,降低数据的维度,从而提高模型的性能。
请你编写一个程序,使用卡方检验对给定的文本数据集进行特征选择。
具体要求如下:
1.读取输入数据集,包含多篇标注了类别的文本文档。
2.提取特征,采用词频(Bag−of−Words)模型,将文本转换为特征向量。(不能忽视单词字母大小写)
3.计算每个特征(词)的卡方统计量,衡量其与类别标签的相关性。
4.根据卡方统计量选择前(k)个最重要的特征。
5.输出选定的特征列表。
输入描述
- 第一行包含一个整数(N),表示文档的数量。
 - 接下来的(N)行,每行包含一个文档,格式为:<label>\t
 - <label>: 文档的类别,取值为'positive'或'negative',
 - \t:一个制表符,分隔类别标签和文档内容。
 - <text>: 文档的内容,由若干单词组成,单词之间用空格分隔。
 - 最后一行包含一个整数(k),表示需要选择的特征数量。
 
输出描述
输出(k)行,每行包含一个单词(特征),按照卡方统计量从大到小排序。如果多个特征的卡方值相同,按字母顺序升序排列。
补充说明
- 卡方检验公式
 
对于每个特征(单词),卡方统计量计算公式为:
x2=∑i=12∑j=12Eij(Oij−Eij)2 其中:
- 
(Oij)是观察到的频数,表示特征是否出现和类别的四种组合情况,
 - 
Eij是期望频数,按照独立性假设计算:
Eij=总样本数(特征是否出现的行数)×(类别的列和)
 
样例1
输入
6
positive I love this movie
negative I hate this movie
positive This film was fantastic
negative This film was terrible
positive What a great experience
negative What a bad experience
3
输出
bad
fantastic
great
说明
- 步骤1:读取6 篇文档及其类别标签。
 - 步骤 2:统计每个单词在不同类别中的出现次数,计算卡方统计量。
 - 步骤3:根据卡方值从大到小排序,选择前3个特征。自测验入
 - 步骤4:输出选定的特征列表。
 
样例2
输入
2
1 1
输出
-1