【问题标题】:Non-recursive enumeration of triply restricted positive integer compositions三重限制正整数组合的非递归枚举
【发布时间】:2019-07-08 21:18:55
【问题描述】:

在创建一个迭代(非递归)函数后,该函数以字典顺序枚举双重限制compositions of positive integers,对于具有非常少量 RAM(但大 EPROM)的微控制器,我不得不将限制数量扩大到3个,即:

  1. 对作品长度的限制
  2. 元素最小值的限制
  3. 元素最大值的限制

下面列出了生成双重限制组合的原始函数:

void GenCompositions(unsigned int myInt, unsigned int CompositionLen, unsigned int MinVal)
{
    if ((MinVal = MinPartitionVal(myInt, CompositionLen, MinVal, (unsigned int) (-1))) == (unsigned int)(-1)) // Increase the MinVal to the minimum that is feasible.
        return;

    std::vector<unsigned int> v(CompositionLen);
    int pos = 0;
    const int last = CompositionLen - 1;


    for (unsigned int i = 1; i <= last; ++i) // Generate the initial composition
        v[i] = MinVal;

    unsigned int MaxVal = myInt - MinVal * last;
    v[0] = MaxVal;

    do
    {
        DispVector(v);

        if (pos == last)
        {
            if (v[last] == MaxVal)
                break;

            for (--pos; v[pos] == MinVal; --pos);  //Search for the position of the Least Significant non-MinVal (not including the Least Significant position / the last position).
            //std::cout << std::setw(pos * 3 + 1) << "" << "v" << std::endl;    //DEBUG

            --v[pos++];
            if (pos != last)
            {
                v[pos] = v[last] + 1;
                v[last] = MinVal;
            }
            else
                v[pos] += 1;

        }
        else
        {
            --v[pos];
            v[++pos] = MinVal + 1;
        }

    } while (true);
}

这个函数的示例输出是:

GenCompositions(10,4,1);:
7, 1, 1, 1
6, 2, 1, 1
6, 1, 2, 1
6, 1, 1, 2
5, 3, 1, 1
5, 2, 2, 1
5, 2, 1, 2
5, 1, 3, 1
5, 1, 2, 2
5, 1, 1, 3
4, 4, 1, 1
4, 3, 2, 1
4, 3, 1, 2
4, 2, 3, 1
4, 2, 2, 2
4, 2, 1, 3
4, 1, 4, 1
4, 1, 3, 2
4, 1, 2, 3
4, 1, 1, 4
3, 5, 1, 1
3, 4, 2, 1
3, 4, 1, 2
3, 3, 3, 1
3, 3, 2, 2
3, 3, 1, 3
3, 2, 4, 1
3, 2, 3, 2
3, 2, 2, 3
3, 2, 1, 4
3, 1, 5, 1
3, 1, 4, 2
3, 1, 3, 3
3, 1, 2, 4
3, 1, 1, 5
2, 6, 1, 1
2, 5, 2, 1
2, 5, 1, 2
2, 4, 3, 1
2, 4, 2, 2
2, 4, 1, 3
2, 3, 4, 1
2, 3, 3, 2
2, 3, 2, 3
2, 3, 1, 4
2, 2, 5, 1
2, 2, 4, 2
2, 2, 3, 3
2, 2, 2, 4
2, 2, 1, 5
2, 1, 6, 1
2, 1, 5, 2
2, 1, 4, 3
2, 1, 3, 4
2, 1, 2, 5
2, 1, 1, 6
1, 7, 1, 1
1, 6, 2, 1
1, 6, 1, 2
1, 5, 3, 1
1, 5, 2, 2
1, 5, 1, 3
1, 4, 4, 1
1, 4, 3, 2
1, 4, 2, 3
1, 4, 1, 4
1, 3, 5, 1
1, 3, 4, 2
1, 3, 3, 3
1, 3, 2, 4
1, 3, 1, 5
1, 2, 6, 1
1, 2, 5, 2
1, 2, 4, 3
1, 2, 3, 4
1, 2, 2, 5
1, 2, 1, 6
1, 1, 7, 1
1, 1, 6, 2
1, 1, 5, 3
1, 1, 4, 4
1, 1, 3, 5
1, 1, 2, 6
1, 1, 1, 7

加上第3个限制(元素的最大值)后,函数的复杂度显着增加。该扩展功能如下:

void GenCompositions(unsigned int myInt, unsigned int CompositionLen, unsigned int MinVal, unsigned int MaxVal)
{
    if ((MaxVal = MaxPartitionVal(myInt, CompositionLen, MinVal, MaxVal)) == 0) //Decrease the MaxVal to the maximum that is feasible.
        return;

    if ((MinVal = MinPartitionVal(myInt, CompositionLen, MinVal, MaxVal)) == (unsigned int)(-1))    //Increase the MinVal to the minimum that is feasible.
        return;

    std::vector<unsigned int> v(CompositionLen);
    unsigned int last = CompositionLen - 1;
    unsigned int rem = myInt - MaxVal - MinVal*(last-1);
    unsigned int pos = 0;

    v[0] = MaxVal;  //Generate the most significant element in the initial composition

    while (rem > MinVal){   //Generate the rest of the initial composition (the highest in the lexicographic order). Spill the remainder left-to-right saturating at MaxVal

        v[++pos] = ( rem > MaxVal ) ? MaxVal : rem;  //Saturate at MaxVal
        rem -= v[pos] - MinVal; //Deduct the used up units (less the background MinValues)
    }

    for (unsigned int i = pos+1; i <= last; i++)    //Fill with MinVal where the spillage of the remainder did not reach.
        v[i] = MinVal;


    if (MinVal == MaxVal){  //Special case - all elements are the same. Only the initial composition is possible.
        DispVector(v);
        return;
    }

    do
    {
        DispVector(v);

        if (pos == last)        
        {       
            for (--pos; v[pos] == MinVal; pos--) {  //Search backwards for the position of the Least Significant non-MinVal (not including the Least Significant position / the last position).
                if (!pos)   
                    return;
            }

            //std::cout << std::setw(pos*3 +1) << "" << "v" << std::endl;  //Debug

            if (v[last] >= MaxVal)  // (v[last] > MaxVal) should never occur
            {

                if (pos == last-1)  //penultimate position. //Skip the iterations that generate excessively large compositions (with elements > MaxVal).
                {   
                    for (rem = MaxVal; ((v[pos] == MinVal) || (v[pos + 1] == MaxVal)); pos--) { //Search backwards for the position of the Least Significant non-extremum (starting from the penultimate position - where the previous "for loop" has finished).  THINK:  Is the (v[pos] == MinVal) condition really necessary here ?
                        rem += v[pos];  //Accumulate the sum of the traversed elements
                        if (!pos)
                            return;
                    }
                    //std::cout << std::setw(pos * 3 + 1) << "" << "v" << std::endl;    //Debug

                    --v[pos];
                    rem -= MinVal*(last - pos - 1) - 1;  //Subtract the MinValues, that are assumed to always be there as a background

                    while (rem > MinVal)    // Spill the remainder left-to-right saturating at MaxVal
                    {
                        v[++pos] = (rem > MaxVal) ? MaxVal : rem;   //Saturate at MaxVal
                        rem -= v[pos] - MinVal; //Deduct the used up units (less the background MinValues)
                    }

                    for (unsigned int i = pos + 1; i <= last; i++)  //Fill with MinVal where the spillage of the remainder did not reach.
                        v[i] = MinVal;

                    continue;   //The skipping of excessively large compositions is complete. Nothing else to adjust...
                }

                /* (pos != last-1) */
                --v[pos];
                v[++pos] = MaxVal;
                v[++pos] = MinVal + 1;  //Propagate the change one step further. THINK: Why a CONSTANT value like MinVal+1 works here at all?

                if (pos != last)
                    v[last] = MinVal;

            }
            else    // (v[last] < MaxVal)
            {           
                --v[pos++];
                if (pos != last)
                {
                    v[pos] = v[last] + 1;
                    v[last] = MinVal;
                }
                else
                    v[pos] += 1;
            }
        }
        else    // (pos != last)
        {
            --v[pos];
            v[++pos] = MinVal + 1;  // THINK: Why a CONSTANT value like MinVal+1 works here at all ?
        }

    } while (true);
}

这个扩展函数的示例输出是:

GenCompositions(10,4,1,4);:
4, 4, 1, 1
4, 3, 2, 1
4, 3, 1, 2
4, 2, 3, 1
4, 2, 2, 2
4, 2, 1, 3
4, 1, 4, 1
4, 1, 3, 2
4, 1, 2, 3
4, 1, 1, 4
3, 4, 2, 1
3, 4, 1, 2
3, 3, 3, 1
3, 3, 2, 2
3, 3, 1, 3
3, 2, 4, 1
3, 2, 3, 2
3, 2, 2, 3
3, 2, 1, 4
3, 1, 4, 2
3, 1, 3, 3
3, 1, 2, 4
2, 4, 3, 1
2, 4, 2, 2
2, 4, 1, 3
2, 3, 4, 1
2, 3, 3, 2
2, 3, 2, 3
2, 3, 1, 4
2, 2, 4, 2
2, 2, 3, 3
2, 2, 2, 4
2, 1, 4, 3
2, 1, 3, 4
1, 4, 4, 1
1, 4, 3, 2
1, 4, 2, 3
1, 4, 1, 4
1, 3, 4, 2
1, 3, 3, 3
1, 3, 2, 4
1, 2, 4, 3
1, 2, 3, 4
1, 1, 4, 4

问题:我对元素最大值限制的实现哪里出错了,导致代码的大小和复杂性增加?
IOW:算法的缺陷在哪里,导致在添加一个简单的&lt;= MaxVal 限制后出现此代码膨胀?不递归可以简化吗?

如果有人想实际编译它,下面列出了辅助函数:

#include <iostream>
#include <iomanip>
#include <vector> 

void DispVector(const std::vector<unsigned int>& partition)
{
    for (unsigned int i = 0; i < partition.size() - 1; i++)       //DISPLAY THE VECTOR HERE ...or do sth else with it.
        std::cout << std::setw(2) << partition[i] << ",";

    std::cout << std::setw(2) << partition[partition.size() - 1] << std::endl;
}

unsigned int MaxPartitionVal(const unsigned int myInt, const unsigned int PartitionLen, unsigned int MinVal, unsigned int MaxVal)
{
    if ((myInt < 2) || (PartitionLen < 2) || (PartitionLen > myInt) || (MaxVal < 1) || (MinVal > MaxVal) || (PartitionLen > myInt) || ((PartitionLen*MaxVal) < myInt ) || ((PartitionLen*MinVal) > myInt))  //Sanity checks
        return 0;

    unsigned int last = PartitionLen - 1;

    if (MaxVal + last*MinVal > myInt)
        MaxVal = myInt - last*MinVal;   //It is not always possible to start with the Maximum Value. Decrease it to sth possible

    return MaxVal;
}

unsigned int MinPartitionVal(const unsigned int myInt, const unsigned int PartitionLen, unsigned int MinVal, unsigned int MaxVal)
{
    if ((MaxVal = MaxPartitionVal(myInt, PartitionLen, MinVal, MaxVal)) == 0)   //Assume that MaxVal has precedence over MinVal
        return (unsigned int)(-1);

    unsigned int last = PartitionLen - 1;

    if (MaxVal + last*MinVal > myInt)
        MinVal = myInt - MaxVal - last*MinVal;  //It is not always possible to start with the Minimum Value. Increase it to sth possible

    return MinVal;
}

//
// Put the definition of GenCompositions() here....
//

int main(int argc, char *argv[])
{
    GenCompositions(10, 4, 1, 4);

    return 0;
}

注意:由这些函数生成的组合的(从上到下)字典顺序不是可选的。 ...也不会跳过不会生成有效组合的“do loop”迭代。

【问题讨论】:

  • 请提供main 函数。
  • @PaulMcKenzie:完成。在辅助函数的末尾。
  • @Silvano:你当然是对的,但我关心的主要是“do loop”中的算法设计
  • 您的函数无法编译。一个例子:return unsigned int (-1); 行上的 "error: expected primary-expression before 'unsigned'"。编译错误很容易修复,但它们的存在使您的问题变得糟糕。
  • @GeorgeRobinson 我将Wandbox 与 gcc 编译器一起使用(我刚刚重新检查了 head、9.1.0 和 4.4.7)。我也尝试过 clang(head 和 8.0.0),虽然错误消息有不同的措辞(“预期 '(' for function-style cast or type construction"),但它仍然不喜欢那样行。使用什么编译器?修复包括括号(return (unsigned int)(-1);)、删除不必要的关键字(return unsigned(-1);)和不那么神秘(return std::numeric_limits&lt;unsigned int&gt;::max();)。

标签: c++ algorithm combinatorics


【解决方案1】:

算法

生成具有有限数量的部分和最小值和最大值的组合的迭代算法并不复杂。固定长度和最小值的结合实际上使事情变得更容易;我们可以始终保持每个部分的最小值,只需移动“额外”值即可生成不同的构图。

我将使用这个例子:

n=15, length=4, min=3, max=5

我们将从创建具有最小值的合成开始:

3,3,3,3

然后我们将剩余的值 15 - 12 = 3 分配到各个部分,从第一部分开始,每次达到最大值时向右移动:

5,4,3,3

这是第一篇作文。然后,我们将使用以下规则重复转换组合以获得反向字典顺序的下一个组合:

我们从寻找值大于最小值的最右边的部分开始每一步。 (其实这可以简化;见本答案末尾的更新代码示例。)如果这部分不是最后一部分,我们从它减去 1,并在右边的部分加 1其中,例如:

5,4,3,3
  ^
5,3,4,3

那是下一个作品。如果最右边的非最小部分是最后一部分,事情会稍微复杂一些。我们将最后一部分的值减至最小值,并将“额外”值存储在临时总计中,例如:

3,4,3,5
      ^
3,4,3,3   + 2

然后我们向左移动,直到找到值大于最小值的下一部分:

3,4,3,3   + 2
  ^

如果这部分(2)右边的部分个数能容纳暂存加1,我们将当前部分减1,暂存加1,然后分配暂存,从当前部分右侧的部分:

3,3,3,3   + 3
    ^
3,3,5,4

那是我们的下一个作品。如果非最小值部分右侧的部分无法保持临时总计加 1,我们将再次将该部分减少到最小值并将“额外”值添加到临时总计中,并进一步查看左,例如(使用 n=17 的不同示例):

5,3,4,5
      ^
5,3,4,3   + 2
    ^
5,3,3,3   + 3
^
4,3,3,3   + 4
  ^
4,5,5,3

那是我们的下一个作品。如果我们向左移动以找到一个非最小值,但在没有找到的情况下到达第一部分,我们已经过了最后一个组合,例如:

3,3,4,5
      ^
3,3,4,3   + 2
    ^
3,3,3,3   + 3
?

这意味着3,3,4,5 是最后一个组合。

如您所见,这仅需要用于一个合成和临时总数的空间,从右到左迭代每个合成一次以找到非最小部分,并从左到右迭代一次合成以分配临时总数。它创建的所有作品都是有效的,并且按相反的字典顺序排列。


代码示例

我首先将上面解释的算法直接翻译成 C++。找到最右边的非最小部分并在组合上分配值是由两个辅助函数完成的。代码一步一步地遵循解释,但这不是最有效的编码方式。请参阅下面的改进版本。

#include <iostream>
#include <iomanip>
#include <vector>

void DisplayComposition(const std::vector<unsigned int>& comp)
{
    for (unsigned int i = 0; i < comp.size(); i++)
        std::cout << std::setw(3) << comp[i];
    std::cout << std::endl;
}

void Distribute(std::vector<unsigned int>& comp, const unsigned int part, const unsigned int max, unsigned int value) {
    for (unsigned int p = part; value && p < comp.size(); ++p) {
        while (comp[p] < max) {
            ++comp[p];
            if (!--value) break;
        }
    }
}

int FindNonMinPart(const std::vector<unsigned int>& comp, const unsigned int part, const unsigned int min) {
    for (int p = part; p >= 0; --p) {
        if (comp[p] > min) return p;
    }
    return -1;
}

void GenerateCompositions(const unsigned n, const unsigned len, const unsigned min, const unsigned max) {
    if (len < 1 || min > max || n < len * min || n > len * max) return;
    std::vector<unsigned> comp(len, min);
    Distribute(comp, 0, max, n - len * min);
    int part = 0;

    while (part >= 0) {
        DisplayComposition(comp);
        if ((part = FindNonMinPart(comp, len - 1, min)) == len - 1) {
            unsigned int total = comp[part] - min;
            comp[part] = min;
            while (part && (part = FindNonMinPart(comp, part - 1, min)) >= 0) {
                if ((len - 1 - part) * (max - min) > total) {
                    --comp[part];
                    Distribute(comp, part + 1, max, total + 1);
                    total = 0;
                    break;
                }
                else {
                    total += comp[part] - min;
                    comp[part] = min;
                }
            }
        }
        else if (part >= 0) {
            --comp[part];
            ++comp[part + 1];
        }
    }
}

int main() {
    GenerateCompositions(15, 4, 3, 5);

    return 0;
}

改进的代码示例

实际上,对FindNonMinPart 的大多数调用都是不必要的,因为在您重新分配值之后,您确切地知道最右边的非最小部分在哪里,并且无需再次搜索它。重新分配额外值也可以简化,不需要函数调用。

以下是考虑到这些因素的更高效的代码版本。它在零件中左右移动,搜索非最小零件,重新分配额外价值并在完成后立即输出作品。它明显比第一个版本快(尽管对DisplayComposition 的调用显然占用了大部分时间)。

#include <iostream>
#include <iomanip>
#include <vector>

void DisplayComposition(const std::vector<unsigned int>& comp)
{
    for (unsigned int i = 0; i < comp.size(); i++)
        std::cout << std::setw(3) << comp[i];
    std::cout << std::endl;
}

void GenerateCompositions(const unsigned n, const unsigned len, const unsigned min, const unsigned max) {

    // check validity of input
    if (len < 1 || min > max || n < len * min || n > len * max) return;

    // initialize composition with minimum value
    std::vector<unsigned> comp(len, min);

    // begin by distributing extra value starting from left-most part
    int part = 0;
    unsigned int carry = n - len * min;

    // if there is no extra value, we are done
    if (carry == 0) {
        DisplayComposition(comp);
        return;
    }

    // move extra value around until no more non-minimum parts on the left
    while (part != -1) {

        // re-distribute the carried value starting at current part and go right
        while (carry) {
            if (comp[part] == max) ++part;
            ++comp[part];
            --carry;
        }

        // the composition is now completed
        DisplayComposition(comp);

        // keep moving the extra value to the right if possible
        // each step creates a new composition
        while (part != len - 1) {
            --comp[part];
            ++comp[++part];
            DisplayComposition(comp);
        }

        // the right-most part is now non-minimim
        // transfer its extra value to the carry value
        carry = comp[part] - min;
        comp[part] = min;

        // go left until we have enough minimum parts to re-distribute the carry value
        while (part--) {

            // when a non-minimum part is encountered
            if (comp[part] > min) {

                // if carry value can be re-distributed, stop going left
                if ((len - 1 - part) * (max - min) > carry) {
                    --comp[part++];
                    ++carry;
                    break;
                }

                // transfer extra value to the carry value
                carry += comp[part] - min;
                comp[part] = min;
            }
        }
    }
}

int main() {
    GenerateCompositions(15, 4, 3, 5);

    return 0;
}

【讨论】:

  • @GeorgeRobinson 嗯,刚刚测试了几个大案例,好像没有比你原来的代码快。也许这是最好的。无论如何,你用什么样的数字来运行这个?
  • 通常,我使用 28 左右的 CompositionLen 和各种 MinVal(但总是 >= 1)和各种 MaxVal。我很少使用巨大的 CompositionLen,但只使用 MaxVal-MinVal
  • @GeorgeRobinson 您可以尝试使用 C 风格的数组,将整数大小限制为您实际需要的大小,诸如此类。但这些都是微优化,可能不会产生太大影响。
  • @GeorgeRobinson 我可以看到位模式的可能性,但我认为您必须为每个 max-min 值编写专用版本的代码才能有效。有一些位技巧可以找到整数中最右边的非零位,并且您可以一次将右侧的所有位设置为 0,因此它可能比需要循环搜索部分更快。跨度>
【解决方案2】:

这个算法可以很容易地使用深度优先搜索和递归算法来实现。因为不能使用递归,所以可以使用栈来模拟函数调用。

这是一种可能的解决方案:

void GenCompositions(
    unsigned int value,
    const unsigned int CompositionLen,
    const unsigned int min,
    const unsigned int max
) {
    using composition_t = std::vector<int>;
    using stackframe = std::pair<composition_t::iterator, unsigned int>;

    // Create a vector with size CompositionLen and fill it with the
    // minimum allowed value
    composition_t composition(CompositionLen, min);

    // Because we may have initialised our composition with non-zero values,
    // we need to decrease the remaining value
    value -= min*CompositionLen;

    // Iterator to where we intend to manipulate our composition
    auto pos = composition.begin();

    // We need the callstack to implement the depth first search in an
    // iterative manner without searching through the composition on
    // every backtrace.
    std::vector<stackframe> callstack;

    // We know, that the composition has a maximum length and so does our
    // callstack. By reserving the memory upfront, we never need to
    // reallocate the callstack when pushing new elements.
    callstack.reserve(CompositionLen);

    // Our main loop
    do {

        // We need to generate a valid composition. To do this, we fill the
        // remaining places of the composition with the maximum allowed
        // values, until the remaining value reaches zero
        for(
            ;
            // Check if we hit the end or the total sum equals the value
            pos != composition.end() && value > 0;
            ++pos
        ) {
            // Whenever we edit the composition, we add a frame to our
            // callstack to be able to revert the changes when backtracking
            callstack.emplace_back(pos, value);

            // calculate the maximum allowed increment to add to the current
            // position in our composition
            const auto diff = std::min(value,max-*pos);

            // *pos might have changed in a previous run, therefore we can
            // not use diff as an offset. Instead we have to assign
            // the correct value.
            *pos = min+diff;
            // We changed our composition, so we have to change the
            // remaining value as well
            value -= diff;
        }

        // If the remaining value is zero we got a correct composition and
        // display it to std::out
        if(value == 0) {
            DisplayVector(
                composition,
                std::distance(composition.begin(), pos),
                min
            );
        }

        // This is our backtracking step. To prevent values below the
        // minimum in our composition we backtrack until we get a value that
        // is higher than the minimum. That way we can decrease this value
        // in the last step by one. Because our for loop that generates the
        // valid composition increases pos once more before exit, we have to
        // look at (pos-1).
        while(*(pos-1) <= min) {
            // If our callstack is empty, we can not backtrack further and
            // terminate the algorithm
            if(callstack.empty()) {
                return;
            }
            // If backtracking is possible, we get tha last values from the
            // callstack and reset our state
            pos = callstack.back().first;
            value = callstack.back().second;
            // After this is done, we remove the last frame from the stack
            callstack.pop_back();
        }

        // The last step is to decrease the value in the composition and
        // increase the remaining value to generate the next composition
        --*(pos-1);
        ++value;
    } while(true);
}

我也改了DisplayVector的接口,这里有一个可能的实现:

// Because we stop stepping deeper in our DFS tree, if the remaining value
// is zero, the composition may have wrong values behind pos. This is no
// problem for us, because we know these values have to be the minimum
// allowed value.
void DisplayVector(const std::vector<int>& vector, size_t pos, int minval) {

    // I prefere to print opening an closing brackets around sequences
    std::cout << "{ ";
    // The ostream_iterator is a addition, that comes with C++17.
    std::ostream_iterator<int> out_it (std::cout, " ");
    // If you can't use C++17, you can use a for loop with an index
    // from 0 to pos to print from the composition
    std::copy_n(vector.begin(), pos, out_it);
    // And a for loop to print vector.size() - pos times the value of minval
    // to fill the rest of your composition
    std::fill_n(out_it, vector.size() - pos, minval);
    std::cout << "}\n";
}

要编译此代码,您需要将标准设置为 C++17 并包含以下标头:

#include <iostream>
#include <vector>
#include <iterator>

GenCompositions() 不使用 C++17 特性,所以如果你不能使用现代编译器,你可以重新实现打印功能并继续。

【讨论】:

  • 不使用递归的整个想法是节省用于堆栈的 RAM。我写道,我的微控制器没有太多 RAM。在 RAM 中模拟堆栈帧有点违背了目的。附言我对新的 DisplayVector() 没有任何问题。
  • 我不知道你的 maxlen 是什么,但是对于小的值,很少使用 ram。调用堆栈使用 sizeof(stackframe)*maxlen 字节的内存。并且堆栈帧只包含一个指针和一个 int。
猜你喜欢
  • 1970-01-01
  • 2019-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多