• PCA分析,及c++代码实现


    本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fengbingchun/article/details/79235028

    主成分分析(Principal Components Analysis, PCA)简介可以参考: http://blog.csdn.net/fengbingchun/article/details/78977202

    以下是PCA的C++实现,参考OpenCV 3.3中的cv::PCA类。

    使用ORL Faces Database作为测试图像。关于ORL Faces Database的介绍可以参考: http://blog.csdn.net/fengbingchun/article/details/79008891 

    pca.hpp:

    #ifndef FBC_NN_PCA_HPP_
    #define FBC_NN_PCA_HPP_

    #include <vector>
    #include <string>

    namespace ANN {

    template<typename T = float>
    class PCA {
    public:
    PCA() = default;
    int load_data(const std::vector<std::vector<T>>& data, const std::vector<T>& labels);
    int set_max_components(int max_components);
    int set_retained_variance(double retained_variance);
    int load_model(const std::string& model);
    int train(const std::string& model);
    // project into the eigenspace, thus the image becomes a "point"
    int project(const std::vector<T>& vec, std::vector<T>& result) const;
    // re-create the image from the "point"
    int back_project(const std::vector<T>& vec, std::vector<T>& result) const;

    private:
    // width,height,eigen_vectors;width,height,eigen_values;width,height,means
    int save_model(const std::string& model) const;
    void calculate_covariance_matrix(std::vector<std::vector<T>>& covar, bool scale = false); // calculate covariance matrix
    int eigen(const std::vector<std::vector<T>>& mat, bool sort_ = true); // calculate eigen vectors and eigen values
    // generalized matrix multiplication: dst = alpha*src1.t()*src2 + beta*src3.t()
    int gemm(const std::vector<std::vector<T>>& src1, const std::vector<std::vector<T>>& src2, double alpha,
    const std::vector<std::vector<T>>& src3, double beta, std::vector<std::vector<T>>& dst, int flags = 0) const;
    int gemm(const std::vector<T>& src1, const std::vector<std::vector<T>>& src2, double alpha,
    const std::vector<T>& src3, double beta, std::vector<T>& dst, int flags = 0) const; // GEMM_2_T: flags = 1
    int normalize(T* dst, int length);
    int computeCumulativeEnergy() const;
    int subtract(const std::vector<T>& vec1, const std::vector<T>& vec2, std::vector<T>& result) const;

    typedef struct Size_ {
    int width;
    int height;
    } Size_;

    std::vector<std::vector<T>> data;
    std::vector<T> labels;
    int samples_num = 0;
    int features_length = 0;
    double retained_variance = -1.; // percentage of variance that PCA should retain
    int max_components = -1; // maximum number of components that PCA should retain
    std::vector<std::vector<T>> eigen_vectors; // eigenvectors of the covariation matrix
    std::vector<T> eigen_values; // eigenvalues of the covariation matrix
    std::vector<T> mean;
    int covar_flags = 0; // when features_length > samples_num, covar_flags is 0, otherwise is 1
    };

    } // namespace ANN

    #endif // FBC_NN_PCA_HPP_
    pca.cpp:
    #include "pca.hpp"
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <cmath>
    #include <memory>
    #include <fstream>
    #include "common.hpp"

    namespace ANN {

    template<typename T>
    int PCA<T>::load_data(const std::vector<std::vector<T>>& data, const std::vector<T>& labels)
    {
    this->samples_num = data.size();
    this->features_length = data[0].size();
    if (samples_num > features_length) {
    fprintf(stderr, "now only support samples_num <= features_length ");
    return -1;
    }

    this->data.resize(this->samples_num);
    for (int i = 0; i < this->samples_num; ++i) {
    this->data[i].resize(this->features_length);
    memcpy(this->data[i].data(), data[i].data(), sizeof(T)* this->features_length);
    }

    this->labels.resize(this->samples_num);
    memcpy(this->labels.data(), labels.data(), sizeof(T)*this->samples_num);

    return 0;
    }

    template<typename T>
    int PCA<T>::set_max_components(int max_components)
    {
    CHECK(data.size() > 0);
    int count = std::min(features_length, samples_num);
    if (max_components > 0) {
    this->max_components = std::min(count, max_components);
    }
    this->retained_variance = -1.;
    }

    template<typename T>
    int PCA<T>::set_retained_variance(double retained_variance)
    {
    CHECK(retained_variance > 0 && retained_variance <= 1);
    this->retained_variance = retained_variance;
    this->max_components = -1;
    }

    template<typename T>
    void PCA<T>::calculate_covariance_matrix(std::vector<std::vector<T>>& covar, bool scale)
    {
    const int rows = samples_num;
    const int cols = features_length;
    const int nsamples = rows;
    double scale_ = 1.;
    if (scale) scale_ = 1. / (nsamples /*- 1*/);

    mean.resize(cols, (T)0.);
    for (int w = 0; w < cols; ++w) {
    for (int h = 0; h < rows; ++h) {
    mean[w] += data[h][w];
    }
    }

    for (auto& value : mean) {
    value = 1. / rows * value;
    }

    // int dsize = ata ? src.cols : src.rows; // ata = false;
    int dsize = rows;
    covar.resize(dsize);
    for (int i = 0; i < dsize; ++i) {
    covar[i].resize(dsize, (T)0.);
    }

    Size_ size{ data[0].size(), data.size() };

    T* tdst = covar[0].data();
    int delta_cols = mean.size();
    T delta_buf[4];
    int delta_shift = delta_cols == size.width ? 4 : 0;
    std::unique_ptr<T[]> buf(new T[size.width]);
    T* row_buf = buf.get();

    for (int i = 0; i < size.height; ++i) {
    const T* tsrc1 = data[i].data();
    const T* tdelta1 = mean.data();

    for (int k = 0; k < size.width; ++k) {
    row_buf[k] = tsrc1[k] - tdelta1[k];
    }

    for (int j = i; j < size.height; ++j) {
    double s = 0;
    const T* tsrc2 = data[j].data();
    const T* tdelta2 = mean.data();

    for (int k = 0; k < size.width; ++k) {
    s += (double)row_buf[k] * (tsrc2[k] - tdelta2[k]);
    }

    tdst[j] = (T)(s * scale_);
    }

    if (i < covar.size()-1) {
    tdst = covar[i + 1].data();
    }
    }
    }

    namespace {

    template<typename _Tp>
    static inline _Tp hypot_(_Tp a, _Tp b)
    {
    a = std::abs(a);
    b = std::abs(b);
    if (a > b) {
    b /= a;
    return a*std::sqrt(1 + b*b);
    }
    if (b > 0) {
    a /= b;
    return b*std::sqrt(1 + a*a);
    }
    return 0;
    }

    } // namespace

    template<typename T>
    int PCA<T>::eigen(const std::vector<std::vector<T>>& mat, bool sort_ = true)
    {
    using _Tp = T; // typedef T _Tp;
    auto n = mat.size();
    for (const auto& m : mat) {
    if (m.size() != n) {
    fprintf(stderr, "mat must be square and it should be a real symmetric matrix ");
    return -1;
    }
    }

    eigen_values.resize(n, (T)0.);
    std::vector<T> V(n*n, (T)0.);
    for (int i = 0; i < n; ++i) {
    V[n * i + i] = (_Tp)1;
    eigen_values[i] = mat[i][i];
    }

    const _Tp eps = std::numeric_limits<_Tp>::epsilon();
    int maxIters{ (int)n * (int)n * 30 };
    _Tp mv{ (_Tp)0 };
    std::vector<int> indR(n, 0), indC(n, 0);
    std::vector<_Tp> A;
    for (int i = 0; i < n; ++i) {
    A.insert(A.begin() + i * n, mat[i].begin(), mat[i].end());
    }

    for (int k = 0; k < n; ++k) {
    int m, i;
    if (k < n - 1) {
    for (m = k + 1, mv = std::abs(A[n*k + m]), i = k + 2; i < n; i++) {
    _Tp val = std::abs(A[n*k + i]);
    if (mv < val)
    mv = val, m = i;
    }
    indR[k] = m;
    }
    if (k > 0) {
    for (m = 0, mv = std::abs(A[k]), i = 1; i < k; i++) {
    _Tp val = std::abs(A[n*i + k]);
    if (mv < val)
    mv = val, m = i;
    }
    indC[k] = m;
    }
    }

    if (n > 1) for (int iters = 0; iters < maxIters; iters++) {
    int k, i, m;
    // find index (k,l) of pivot p
    for (k = 0, mv = std::abs(A[indR[0]]), i = 1; i < n - 1; i++) {
    _Tp val = std::abs(A[n*i + indR[i]]);
    if (mv < val)
    mv = val, k = i;
    }
    int l = indR[k];
    for (i = 1; i < n; i++) {
    _Tp val = std::abs(A[n*indC[i] + i]);
    if (mv < val)
    mv = val, k = indC[i], l = i;
    }

    _Tp p = A[n*k + l];
    if (std::abs(p) <= eps)
    break;
    _Tp y = (_Tp)((eigen_values[l] - eigen_values[k])*0.5);
    _Tp t = std::abs(y) + hypot_(p, y);
    _Tp s = hypot_(p, t);
    _Tp c = t / s;
    s = p / s; t = (p / t)*p;
    if (y < 0)
    s = -s, t = -t;
    A[n*k + l] = 0;

    eigen_values[k] -= t;
    eigen_values[l] += t;

    _Tp a0, b0;

    #undef rotate
    #define rotate(v0, v1) a0 = v0, b0 = v1, v0 = a0*c - b0*s, v1 = a0*s + b0*c

    // rotate rows and columns k and l
    for (i = 0; i < k; i++)
    rotate(A[n*i + k], A[n*i + l]);
    for (i = k + 1; i < l; i++)
    rotate(A[n*k + i], A[n*i + l]);
    for (i = l + 1; i < n; i++)
    rotate(A[n*k + i], A[n*l + i]);

    // rotate eigenvectors
    for (i = 0; i < n; i++)
    rotate(V[n*k + i], V[n*l + i]);

    #undef rotate

    for (int j = 0; j < 2; j++) {
    int idx = j == 0 ? k : l;
    if (idx < n - 1) {
    for (m = idx + 1, mv = std::abs(A[n*idx + m]), i = idx + 2; i < n; i++) {
    _Tp val = std::abs(A[n*idx + i]);
    if (mv < val)
    mv = val, m = i;
    }
    indR[idx] = m;
    }
    if (idx > 0) {
    for (m = 0, mv = std::abs(A[idx]), i = 1; i < idx; i++) {
    _Tp val = std::abs(A[n*i + idx]);
    if (mv < val)
    mv = val, m = i;
    }
    indC[idx] = m;
    }
    }
    }

    // sort eigenvalues & eigenvectors
    if (sort_) {
    for (int k = 0; k < n - 1; k++) {
    int m = k;
    for (int i = k + 1; i < n; i++) {
    if (eigen_values[m] < eigen_values[i])
    m = i;
    }
    if (k != m) {
    std::swap(eigen_values[m], eigen_values[k]);
    for (int i = 0; i < n; i++)
    std::swap(V[n*m + i], V[n*k + i]);
    }
    }
    }

    eigen_vectors.resize(n);
    for (int i = 0; i < n; ++i) {
    eigen_vectors[i].resize(n);
    eigen_vectors[i].assign(V.begin() + i * n, V.begin() + i * n + n);
    }

    return 0;
    }

    template<typename T>
    int PCA<T>::gemm(const std::vector<std::vector<T>>& src1, const std::vector<std::vector<T>>& src2, double alpha,
    const std::vector<std::vector<T>>& src3, double beta, std::vector<std::vector<T>>& dst, int flags) const
    {
    CHECK(flags == 0); // now only support flags = 0
    CHECK(typeid(T).name() == typeid(double).name() || typeid(T).name() == typeid(float).name()); // T' type can only be float or double
    CHECK(beta == 0. && src3.size() == 0);

    Size_ a_size{ src1[0].size(), src1.size() }, d_size{ src2[0].size(), a_size.height };
    int len{ (int)src2.size() };
    CHECK(a_size.height == len);
    CHECK(d_size.height == dst.size() && d_size.width == dst[0].size());

    for (int y = 0; y < d_size.height; ++y) {
    for (int x = 0; x < d_size.width; ++x) {
    dst[y][x] = 0.;
    for (int t = 0; t < d_size.height; ++t) {
    dst[y][x] += src1[y][t] * src2[t][x];
    }

    dst[y][x] *= alpha;
    }
    }

    return 0;
    }
    template<typename T>
    int PCA<T>::gemm(const std::vector<T>& src1, const std::vector<std::vector<T>>& src2, double alpha,
    const std::vector<T>& src3, double beta, std::vector<T>& dst, int flags = 0) const
    {
    CHECK(flags == 0 || flags == 1); // when flags = 1, GEMM_2_T
    CHECK(typeid(T).name() == typeid(double).name() || typeid(T).name() == typeid(float).name()); // T' type can only be float or double

    Size_ a_size{ src1.size(), 1 }, d_size;
    int len = 0;

    switch (flags) {
    case 0:
    d_size = Size_{ src2[0].size(), a_size.height };
    len = src2.size();
    CHECK(a_size.width == len);
    break;
    case 1:
    d_size = Size_{ src2.size(), a_size.height };
    len = src2[0].size();
    CHECK(a_size.width == len);
    break;
    }

    if (!src3.empty()) {
    CHECK(src3.size() == d_size.width);
    }

    dst.resize(d_size.width);

    const T* src3_ = nullptr;
    std::vector<T> tmp(dst.size(), (T)0.);
    if (src3.empty()) {
    src3_ = tmp.data();
    } else {
    src3_ = src3.data();
    }

    if (src1.size() == src2.size()) {
    for (int i = 0; i < dst.size(); ++i) {
    dst[i] = (T)0.;
    for (int j = 0; j < src2.size(); ++j) {
    dst[i] += src1[j] * src2[j][i];
    }
    dst[i] *= alpha;
    dst[i] += beta * src3_[i];
    }
    } else {
    for (int i = 0; i < dst.size(); ++i) {
    dst[i] = (T)0.;
    for (int j = 0; j < src1.size(); ++j) {
    dst[i] += src1[j] * src2[i][j];
    }
    dst[i] *= alpha;
    dst[i] += beta * src3_[i];
    }
    }

    return 0;
    }

    template<typename T>
    int PCA<T>::normalize(T* dst, int length)
    {
    T s = (T)0., a = (T)1.;
    for (int i = 0; i < length; ++i) {
    s += dst[i] * dst[i];
    }

    s = std::sqrt(s);
    s = s > DBL_EPSILON ? a / s : 0.;

    for (int i = 0; i < length; ++i) {
    dst[i] *= s;
    }

    return 0;
    }

    template<typename T>
    int PCA<T>::computeCumulativeEnergy() const
    {
    std::vector<T> g(eigen_values.size(), (T)0.);
    for (int ig = 0; ig < eigen_values.size(); ++ig) {
    for (int im = 0; im <= ig; ++im) {
    g[ig] += eigen_values[im];
    }
    }

    int L{ 0 };
    for (L = 0; L < eigen_values.size(); ++L) {
    double energy = g[L] / g[eigen_values.size() - 1];
    if (energy > retained_variance) break;
    }

    L = std::max(2, L);

    return L;
    }

    template<typename T>
    int PCA<T>::train(const std::string& model)
    {
    CHECK(retained_variance > 0. || max_components > 0);
    int count = std::min(features_length, samples_num), out_count = count;
    if (max_components > 0) out_count = std::min(count, max_components);
    covar_flags = 0;
    if (features_length <= samples_num) covar_flags = 1;

    std::vector<std::vector<T>> covar(count); // covariance matrix
    calculate_covariance_matrix(covar, true);
    eigen(covar, true);

    std::vector<std::vector<T>> tmp_data(samples_num), evects1(count);
    for (int i = 0; i < samples_num; ++i) {
    tmp_data[i].resize(features_length);
    evects1[i].resize(features_length);

    for (int j = 0; j < features_length; ++j) {
    tmp_data[i][j] = data[i][j] - mean[j];
    }
    }

    gemm(eigen_vectors, tmp_data, 1., std::vector<std::vector<T>>(), 0., evects1, 0);

    eigen_vectors.resize(evects1.size());
    for (int i = 0; i < eigen_vectors.size(); ++i) {
    eigen_vectors[i].resize(evects1[i].size());
    memcpy(eigen_vectors[i].data(), evects1[i].data(), sizeof(T)* evects1[i].size());
    }

    // normalize all eigenvectors
    if (retained_variance > 0) {
    for (int i = 0; i < eigen_vectors.size(); ++i) {
    normalize(eigen_vectors[i].data(), eigen_vectors[i].size());
    }

    // compute the cumulative energy content for each eigenvector
    int L = computeCumulativeEnergy();
    eigen_values.resize(L);
    eigen_vectors.resize(L);
    } else {
    for (int i = 0; i < out_count; ++i) {
    normalize(eigen_vectors[i].data(), eigen_vectors[i].size());
    }

    if (count > out_count) {
    eigen_values.resize(out_count);
    eigen_vectors.resize(out_count);
    }
    }

    save_model(model);

    return 0;
    }

    template<typename T>
    int PCA<T>::subtract(const std::vector<T>& vec1, const std::vector<T>& vec2, std::vector<T>& result) const
    {
    CHECK(vec1.size() == vec2.size() && vec1.size() == result.size());

    for (int i = 0; i < vec1.size(); ++i) {
    result[i] = vec1[i] - vec2[i];
    }

    return 0;
    }

    template<typename T>
    int PCA<T>::project(const std::vector<T>& vec, std::vector<T>& result) const
    {
    CHECK(!mean.empty() && !eigen_vectors.empty() && mean.size() == vec.size());

    std::vector<T> tmp_data(mean.size());
    subtract(vec, mean, tmp_data);

    gemm(tmp_data, eigen_vectors, 1, std::vector<T>(), 0, result, 1);

    return 0;
    }

    template<typename T>
    int PCA<T>::back_project(const std::vector<T>& vec, std::vector<T>& result) const
    {
    CHECK(!mean.empty() && !eigen_vectors.empty() && eigen_vectors.size() == vec.size());
    gemm(vec, eigen_vectors, 1, mean, 1, result, 0);

    return 0;
    }

    template<typename T>
    int PCA<T>::load_model(const std::string& model)
    {
    std::ifstream file(model.c_str(), std::ios::in | std::ios::binary);
    if (!file.is_open()) {
    fprintf(stderr, "open file fail: %s ", model.c_str());
    return -1;
    }

    int width = 0, height = 0;
    file.read((char*)&width, sizeof(width) * 1);
    file.read((char*)&height, sizeof(height) * 1);
    std::unique_ptr<T[]> data(new T[width * height]);
    file.read((char*)data.get(), sizeof(T)* width * height);
    eigen_vectors.resize(height);
    for (int i = 0; i < height; ++i) {
    eigen_vectors[i].resize(width);
    T* p = data.get() + i * width;
    memcpy(eigen_vectors[i].data(), p, sizeof(T)* width);
    }

    file.read((char*)&width, sizeof(width));
    file.read((char*)&height, sizeof(height));
    CHECK(height == 1);
    eigen_values.resize(width);
    file.read((char*)eigen_values.data(), sizeof(T)* width * height);

    file.read((char*)&width, sizeof(width));
    file.read((char*)&height, sizeof(height));
    CHECK(height == 1);
    mean.resize(width);
    file.read((char*)mean.data(), sizeof(T)* width * height);

    file.close();

    return 0;
    }

    template<typename T>
    int PCA<T>::save_model(const std::string& model) const
    {
    std::ofstream file(model.c_str(), std::ios::out | std::ios::binary);
    if (!file.is_open()) {
    fprintf(stderr, "open file fail: %s ", model.c_str());
    return -1;
    }

    int width = eigen_vectors[0].size(), height = eigen_vectors.size();
    std::unique_ptr<T[]> data(new T[width * height]);
    for (int i = 0; i < height; ++i) {
    T* p = data.get() + i * width;
    memcpy(p, eigen_vectors[i].data(), sizeof(T) * width);
    }
    file.write((char*)&width, sizeof(width));
    file.write((char*)&height, sizeof(height));
    file.write((char*)data.get(), sizeof(T)* width * height);

    width = eigen_values.size(), height = 1;
    file.write((char*)&width, sizeof(width));
    file.write((char*)&height, sizeof(height));
    file.write((char*)eigen_values.data(), sizeof(T)* width * height);

    width = mean.size(), height = 1;
    file.write((char*)&width, sizeof(width));
    file.write((char*)&height, sizeof(height));
    file.write((char*)mean.data(), sizeof(T)* width * height);

    file.close();
    return 0;
    }

    template class PCA<float>;
    template class PCA<double>;

    } // namespace ANN
    main.cpp:
    #include "funset.hpp"
    #include <iostream>
    #include "perceptron.hpp"
    #include "BP.hpp""
    #include "CNN.hpp"
    #include "linear_regression.hpp"
    #include "naive_bayes_classifier.hpp"
    #include "logistic_regression.hpp"
    #include "common.hpp"
    #include "knn.hpp"
    #include "decision_tree.hpp"
    #include "pca.hpp"
    #include <opencv2/opencv.hpp>
    // =============================== PCA(Principal Components Analysis) ===================
    namespace {
    void normalize(const std::vector<float>& src, std::vector<unsigned char>& dst)
    {
    dst.resize(src.size());
    double dmin = 0, dmax = 255;
    double smin = src[0], smax = smin;
    for (int i = 1; i < src.size(); ++i) {
    if (smin > src[i]) smin = src[i];
    if (smax < src[i]) smax = src[i];
    }
    double scale = (dmax - dmin) * (smax - smin > DBL_EPSILON ? 1. / (smax - smin) : 0);
    double shift = dmin - smin * scale;
    for (int i = 0; i < src.size(); ++i) {
    dst[i] = static_cast<unsigned char>(src[i] * scale + shift);
    }
    }
    } // namespace
    int test_pca()
    {
    const std::string image_path{ "E:/GitCode/NN_Test/data/database/ORL_Faces/" };
    const std::string image_name{ "1.pgm" };
    std::vector<cv::Mat> images;
    for (int i = 1; i <= 15; ++i) {
    std::string name = image_path + "s" + std::to_string(i) + "/" + image_name;
    cv::Mat mat = cv::imread(name, 0);
    if (!mat.data) {
    fprintf(stderr, "read image fail: %s ", name.c_str());
    return -1;
    }
    images.emplace_back(mat);
    }
    save_images(images, "E:/GitCode/NN_Test/data/pca_src.jpg", 5);
    cv::Mat data(images.size(), images[0].rows * images[0].cols, CV_32FC1);
    for (int i = 0; i < images.size(); ++i) {
    cv::Mat image_row = images[i].clone().reshape(1, 1);
    cv::Mat row_i = data.row(i);
    image_row.convertTo(row_i, CV_32F);
    }
    int features_length = images[0].rows * images[0].cols;
    std::vector<std::vector<float>> data_(images.size());
    std::vector<float> labels(images.size(), 0.f);
    for (int i = 0; i < images.size(); ++i) {
    data_[i].resize(features_length);
    memcpy(data_[i].data(), data.row(i).data, sizeof(float)* features_length);
    }

    const std::string save_model_file{ "E:/GitCode/NN_Test/data/pca.model" };
    ANN::PCA<float> pca;
    pca.load_data(data_, labels);
    double retained_variance{ 0.95 };
    pca.set_retained_variance(retained_variance);
    pca.train(save_model_file);
    const std::string read_model_file{ save_model_file };
    ANN::PCA<float> pca2;
    pca2.load_model(read_model_file);
    std::vector<cv::Mat> result(images.size());
    for (int i = 0; i < images.size(); ++i) {
    std::vector<float> point, reconstruction;
    pca2.project(data_[i], point);
    pca2.back_project(point, reconstruction);
    std::vector<unsigned char> dst;
    normalize(reconstruction, dst);
    cv::Mat tmp(images[i].rows, images[i].cols, CV_8UC1, dst.data());
    tmp.copyTo(result[i]);
    }
    save_images(result, "E:/GitCode/NN_Test/data/pca_result.jpg", 5);
    return 0;
    }
    执行结果如下,上三行为原始图像,下三行为使用PCA重建图像的结果,经比较与OpenCV 3.3结果一致:

    GitHub: https://github.com/fengbingchun/NN_Test
    ---------------------
    作者:fengbingchun
    来源:CSDN
    原文:https://blog.csdn.net/fengbingchun/article/details/79235028
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    img 的data-src 属性及懒加载
    try catch 用法
    input 的各种属性的验证 checkValidity兼容性
    表单提交的方法。
    通信原理
    计算机组成原理
    CREC 2017
    POJ 1201 Intervals
    HDU 3440 House Man
    poj 3169 Layout
  • 原文地址:https://www.cnblogs.com/hjj-fighting/p/11064032.html
Copyright © 2020-2023  润新知