11.11二分&&前缀和补充

11/11

每日一题:1547.切棍子的最小成本

思路:区间 DP:从记忆化搜索到递推

一、寻找子问题
示例 1 的 cuts=[1,3,4,5],为方便描述,把 0 和 n=7 也视作切开的位置(木棍端点),得到 cuts=[0,1,3,4,5,7]。我们要解决的问题(原问题)是:

切割一根左端点为 cuts[0]=0,右端点为 cuts[5]=7 的棍子的最小成本。
第一刀切在哪?枚举:

  • 在 cuts[1]=1 切一刀,木棍分成两段。第一段左端点为 cuts[0]=0,右端点为 cuts[1]=1;第二段左端点为 cuts[1]=1,右端点为 cuts[5]=7。

  • 在 cuts[2]=3 切一刀,木棍分成两段。第一段左端点为 cuts[0]=0,右端点为 cuts[2]=3;第二段左端点为 cuts[2]=3,右端点为 cuts[5]=7。

  • 在 cuts[3]=4 切一刀,木棍分成两段。第一段左端点为 cuts[0]=0,右端点为 cuts[3]=4;第二段左端点为 cuts[3]=4,右端点为 cuts[5]=7。

  • 在 cuts[4]=5 切一刀,木棍分成两段。第一段左端点为 cuts[0]=0,右端点为 cuts[4]=5;第二段左端点为 cuts[4]=5,右端点为 cuts[5]=7。

    接下来,继续计算这两段木棍各自的最小切割成本。同样地,枚举切割的位置。依此类推。这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。

注:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。本题用到的是「枚举选哪个」。

二、状态定义与状态转移方程
根据上面的讨论,我们需要在递归过程中,知道当前切的这根棍子,左端点在哪,右端点在哪。

因此,定义状态为 dfs(i,j),表示切割一根左端点为 cuts[i],右端点为 cuts[j] 的棍子的最小成本。

枚举在 cuts[k] 处切一刀,其中 k=i+1,i+2,…,j−1,木棍变成两段:

第一段左端点为 cuts[i],右端点为 cuts[k],切割这段木棍的最小成本为 dfs(i,k)
第二段左端点为 cuts[k],右端点为 cuts[j],切割这段木棍的最小成本为 dfs(k,j)
成本之和为 dfs(i,k)+dfs(k,j),再算上切割之前木棍的长度 cuts[j]−cuts[i],得到
dfs(i,k)+dfs(k,j)+cuts[j]−cuts[i]。枚举 k=i+1,i+2,…,j−1,所有成本取最小值,得\(dfs(i,j)= \min^{j−1}_{k = i+1} dfs(i,k)+dfs(k,j)+cuts[j]−cuts[i]\)

其中 cuts[j]−cuts[i] 与 k 无关,可以提到循环外面。

递归边界dfs(i,i+1)=0。此时木棍中没有要切割的位置,所以切割成本为 0。

递归入口dfs(0,m−1),也就是答案。其中 m 是添加了 0 和 n 之后的 cuts 数组的长度。

三、递归搜索 + 保存递归返回值 = 记忆化搜索
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:

如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 memo 数组中。
如果一个状态不是第一次遇到(memo 中保存的结果不等于 memo 的初始值),那么可以直接返回 memo 中保存的结果。

注意:memo 数组的初始值一定不能等于要记忆化的值!例如初始值设置为 0,并且要记忆化的 dfs(i,j) 也等于 0,那就没法判断 0 到底表示第一次遇到这个状态,还是表示之前遇到过了,从而导致记忆化失效。一般把初始值设置为 −1。本题由于 cuts[j]−cuts[i]>0,所以除了递归边界以外,dfs 的返回值均为正数,所以也可以把初始值设置为 0。

四.复杂度分析
时间复杂度:\(O(m ^ 3)\),其中 m 为 cuts 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 = 状态个数 × 单个状态的计算时间。本题状态个数等于 \(O(m ^ 2 )\),单个状态的计算时间为 O(m),所以总的时间复杂度为$ O(m ^ 3 )\(。 空间复杂度:\)O(m^ 2)$。保存多少状态,就需要多少空间。

五、1:1 翻译成递推(DP法)
我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体来说,f[i][j] 的定义和 dfs(i,j) 的定义是一样的,都表示切割一根左端点为 cuts[i],右端点为cuts[j]的棍子的最小成本。

相应的递推式(状态转移方程)也和 dfs 一样:

\(f[i][j]= \min^{j−1}_{k=i+1} f[i][k]+f[k][j]+cuts[j]−cuts[i] \)

初始值 f[i][i+1]=0,翻译自递归边界 dfs(i,i+1)=0

答案为 f[0][m−1],翻译自递归入口 dfs(0,m−1)

问:如何思考循环顺序?什么时候要正序枚举,什么时候要倒序枚举?

答:这里有一个通用的做法:盯着状态转移方程,想一想,要计算 f[i][j],必须先把 f[k][j] 算出来,由于 i<k,那么只有 i 从大到小枚举才能做到。同理,必须先把同一行的 f[i][k] 算出来,由于 j>k,那么只有 j 从小到大枚举才能做到。

法一:记忆化搜索

class Solution {
public:
    int minCost(int n, vector<int>& cuts) {
        cuts.push_back(0);
        cuts.push_back(n);
        ranges::sort(cuts);

        int m = cuts.size();
        vector<vector<int>> memo(m , vector<int>(m));
        auto dfs = [&](auto& dfs , int i , int j) -> int {
          if(i + 1 == j)  return 0;
          int& res = memo[i][j];
          if(res) return res;

          res = INT_MAX;
          for (int k = i + 1; k < j; k++) {
             res = min(res , dfs(dfs , i , k) + dfs(dfs , k , j));
          }
          res += cuts[j] - cuts[i];
          return res;
        };
        return dfs(dfs , 0 , m - 1);
    }
};

法二:DP

class Solution {
public:
    int minCost(int n, vector<int>& cuts) {
       cuts.push_back(0);
       cuts.push_back(n);
       ranges::sort(cuts);

       int m = cuts.size();
       vector<vector<int>> dp(m , vector<int>(m));
       for (int i = m - 3; i >= 0 ; i--) {
            for (int j = i + 2; j < m; j++) {
               int res = INT_MAX;
               for (int k = i + 1 ; k < j ; k ++){
                res = min(res , dp[i][k] + dp[k][j]);
               }
               dp[i][j] = res + cuts[j] - cuts[i];
            }
       }
       return dp[0][m - 1];
    }
};

代码注释:

C++20新特性:ranges::sort,让排序更简洁高效(正序、逆序、自定义排序)

range::sort(cuts);
//等价于
sort(cuts.begin() , cuts.end());

// 传统逆序排序方式
std::sort(numbers.rbegin(), numbers.rend());
// 使用 ranges::sort 进行逆序排序
std::ranges::sort(numbers, std::greater<>());

// 传统自定义排序方式
bool customCompare(int a, int b) {
 // 自定义排序规则
 return a % 3 < b % 3;
}

std::sort(numbers.begin(), numbers.end(), customCompare);
//或者用lambda函数写成
// sort(a, a+n, [](int a,int b){return a>b;});//降序排序

// 使用 ranges::sort 进行自定义排序
 std::ranges::sort(numbers, [](int a, int b) {
     // 自定义排序规则
     return a % 3 < b % 3;
 });

捕获选项

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型

所谓捕获列表,其实可以理解为参数的一种类型,lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用

  • [] Capture nothing (or, a scorched earth strategy?)
  • [&] Capture any referenced variable by reference
  • [=] Capture any referenced variable by making a copy
  • [=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference
  • [bar] Capture bar by making a copy; don’t copy anything else
  • [this] Capture the this pointer of the enclosing class
捕获形式 说明
[] 不捕获任何外部变量
[变量名, …] 默认以值的形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符)
[this] 以值的形式捕获this指针
[=] 以值的形式捕获所有外部变量
[&] 以引用形式捕获所有外部变量
[=, &x] 变量x以引用形式捕获,其余变量以传值形式捕获
[&, x] 变量x以值的形式捕获,其余变量以引用形式捕获

1. 值捕获

#include <iostream>
using namespace std;

void learn_lambda_func_3() {
	auto add = [v1 = 1.2, v2 = 2](int x, int y) -> double{
		return x + y + v1 + v2;
	};
	std::cout << "add(3, 4) = " << add(3, 4) << std::endl;
}

int main()
{
	learn_lambda_func_3();

	return 0;
}

输出:

add(3, 4) = 10.2

与参数传值类似,值捕获的前期是变量可以拷贝。不同之处则在于,被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝:

#include <iostream>
using namespace std;

void learn_lambda_func_1() {
	int value_1 = 1;
	auto copy_value_1 = [value_1] {
		return value_1;
	};
	value_1 = 100;
	auto stored_value_1 = copy_value_1();
	// 这时, stored_value_1 == 1, 而 value_1 == 100.
	// 因为 copy_value_1 在创建时就保存了一份 value_1 的拷贝
	cout << "value_1 = " << value_1 << endl;
	cout << "stored_value_1 = " << stored_value_1 << endl;
}

int main()
{
	learn_lambda_func_1();
	return 0;
}

输出结果:

value_1 = 100
stored_value_1 = 1

2. 引用捕获

与引用传参类似,引用捕获保存的是引用,值会发生变化。

void learn_lambda_func_2() {
	int value_2 = 1;
	auto copy_value_2 = [&value_2] {
		return value_2;
	};
	value_2 = 100;
	auto stored_value_2 = copy_value_2();
	// 这时, stored_value_2 == 100, value_1 == 100.
	// 因为 copy_value_2 保存的是引用
	cout << "value_2 = " << value_2 << endl;
	cout << "stored_value_2 = " << stored_value_2 << endl;
}

输出结果:

value_2 = 100
stored_value_2 = 100

3. 隐式捕获

手动书写捕获列表有时候是非常复杂的,需要我们在捕获列表中显示列出Lambda表达式中使用的外部变量。但是,开发者想了一个办法,就是将这种机械性的工作可以交给编译器来处理。让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。

隐式捕获有两种方式,分别是[=][&][=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

#include <iostream>
using namespace std;

class test
{
public:
	void hello() {
		cout << "test hello!\n";
	};
	void lambda() {
		auto fun = [this] { // 捕获了 this 指针
			this->hello(); // 这里 this 调用的就是 class test 的对象了
		};
		fun();
	}
};

int main()
{
	//显示传递
	cout << "/*******显示传递*******/" << endl;
	int i = 12;		cout << &i << endl;			//输出:010FF834	//012(8进制)十进制是10。
	auto func = [i] { cout  << &i << " " << i <<endl; };
	func();                                 // 输出:010FF828 12

	cout << endl;

	//隐式传递
	cout << "/*******隐式传递*******/" << endl;

	[](int j) { cout << &j << " " << j << endl; }(23);      //或通过“函数体”后面的‘()’传入参数
	cout << endl;

	cout << "1.拷贝捕获" << endl;
	int a = 123; cout << &a << endl;         //输出:010FF81C
	auto f = [=] { cout << &a << " "<<a << endl; };    
	f();                                     //输出:010FF810 123
	cout << endl;

	cout << "2.引用捕获" << endl;
	int b = 234;
	auto f_1 = [&] { cout << &b << " " << b<<endl; }; 
	b = 345; cout << &b << endl;				//输出:010FF804
	f_1();                                    //输出:010FF804 345
	cout << endl;

	cout << "3.拷贝与引用混合" << endl;
	//[&, x],变量x以引用形式捕获,其余变量以传值形式捕获
	int c = 456, d = 567;					
	cout << &c << " " << &d << endl;          //输出:010FF7EC 010FF7E0
	auto f_2 = [=, &d] {					
		cout << &c << " " << &d <<" "<< c << " " << d << endl;       //输出:010FF7D4 010FF7E0 456 567
	};
	f_2();
	cout << endl;

	cout << "4.[bar] 指定引用或拷贝" << endl;
	int e = 678;    cout << &e << endl;       //输出:010FF7C4
	auto f_3 = [e] { cout << &e << " " << e << endl; };
	f_3();									  // 输出:010FF7B8 678
	cout << endl;

	cout << "5.[this] 捕获 this 指针" << endl;
	//我们要跳到类中了
	test t;
	t.lambda();

	return 0;
}

Lambda表达式还支持混合的方式捕获外部变量,这种方式主要是以上几种捕获方式的组合使用。

修改捕获变量

前面我们提到过,在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。那么有没有办法可以修改值捕获的外部变量呢?这是就需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量,示例:

int main()
{
	int a = 123; cout << &a << endl;
	auto f = [a]()mutable {cout << ++a <<endl; };//不会报错
	cout << a << endl;//输出123
	f();//输出124
	cout << &a << endl;
}

输出结果:

006FF954
123
124
006FF954

嵌套 Lambda 表达式

你可以将 lambda 表达式嵌套在另一个中,如下例所示。 内部 lambda 表达式将其自变量与 2 相乘并返回结果。 外部 lambda 表达式通过其自变量调用内部 lambda 表达式并在结果上加 3。

// nesting_lambda_expressions.cpp
// compile with: /EHsc /W4
#include <iostream>
using namespace std;

int main()
{
    // The following lambda expression contains a nested lambda
    // expression.
    int timestwoplusthree = [](int x) { return [](int y) { return y * 2; }(x) + 3; }(5);
    // Print the result.
    cout << timestwoplusthree << endl;
}

输出:

13

泛型 Lambda (C++14)

我们曾提到了 auto 关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生冲突。但是 Lambda 表达式并不是普通函数,所以 Lambda 表达式并不能够模板化。这就为我们造成了一定程度上的麻烦:参数表不能够泛化,必须明确参数表类型。

幸运的是,这种麻烦只存在于 C++11 中,从 C++14 开始,Lambda 函数的形式参数可以使用 auto 关键字来产生意义上的泛型:

void learn_lambda_func_4() {
	auto generic = [](auto x, auto y) {
		return x + y;
	};

	std::cout << "generic(1,2) = " << generic(1, 2) << std::endl;
	std::cout << "generic(1.1,2.2) = " << generic(1.1, 2.2) << std::endl;
}

输出:

generic(1,2) = 3
generic(1.1,2.2) = 3.3


99.激光炸弹

思路:二维前缀和

Sum(矩阵和) = s[x2][y2] - s[x2][y1- 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]

image-20241111174515481

本题的题意较为模糊,题解依旧沿用原解释:不能炸掉正方形边上的点。

因此边长为R的正方形,实际上可以摧毁的面积为 (R - 1)^2。

那么本题中 x2=i , y2 = j , x1 = x2 - r + 1 , y1 = y2 - r + 1

代入得:

res = max(res , s[i][j] - s[i][j - r] - s[i - r][j] + s[i - r][j - r]);
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;
const int N = 5010;
int s[N][N];

int main(){
  int n , r;
  scanf("%d%d" , &n, &r);
  r = min(r , 5001);//如果r>5001则能覆盖到所有的点,直接输出即可。

  int x , y , w;
  for (int i = 0; i < n; i++) {
     cin >> x >> y >> w;
    //因为求前缀和是从1开始,相当于把点(x ,y)平移到(x + 1 , y + 1)处
     x ++ , y ++;
     s[x][y] += w;//因为Xi , Yi可能重合
  }

  for (int i = 1; i <= 5001; i++) {
     for( int j = 1 ; j <= 5001 ; j ++){
      s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] ;//这里省去了分别创建a ,s数组的过程,直接把前缀和迭代到原数组里。
     }
  }

  int res = 0;
  for (int i = r; i <= 5001 ; i++) {
      for (int j = r; j <= 5001; j++) {
         res  = max(res , s[i][j] - s[i - r][j] - s[i][j - r] + s[i - r][j - r]);
        //求子矩阵的和里的公式。
      }
  }

  cout << res;
  return 0;
}

100.增减序列

思路:差分+贪心

本题难在思路,公式的推导。

image-20241111194425322

  1. 问题的转化:把一个区间所有的数+/- 1 -> 对差分数组 l 、r+1 两个数进行加减操作,因此可以简化。
  2. 最后要求所有数相同,对应差分数组即:b1、bn+1任意,b2~bn所有数为0
  3. 原问题即问,至少操作多少次可使b2~bn都为0,b1有多少种值。

分析完毕,接下来看对b数组的操作,分为4种:记对a区间左右下标为L、R进行加减,实际要进行加减的数为bl、b(r+1)。因为每次只能对b加减1,记b2-bn中所有正数和为P,负数和为q。

  • 2 <= L <=R <= n - 1 在b2~bn中令某数+1,另一个数-1。 可以进行min{p , q}次,直到另一个为0。
  • L = 1 R <= n - 1 b1 和 b2~bn 中的某数 +/-1 。可以进行abs(p - q)次
  • 2 <= L R = n b(n+1)和b2~bn中的某数+/- 1 。可以进行abs(p - q)次
  • L = 1 R =n 对所有数加减一,无意义。

最小次数 = min{p , q} + abs(p - q) = max{p , q}

b1的取值取决于情况2、3进行的次数,情况2 可以进行0~abs(p - q)次,因此取值有abs(p-q) + 1种可能

对于题目数的定义:0≤ai<2147483648 ,因此定义为int即可。而p、q需定义为long long 型。

常见数的范围:

int:-2147483648~2147483647 ,是10位数

long : -2147483648~2147483647 (后来32位的操作系统中,int 占4字节,范围与 int 相同; )

long long: -9223372036854775808 ~ 9223372036854775807

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N] , b[N];

int main(){
  int n ;
  scanf("%d" , &n);
  for (int i = 1; i <= n; i++) {
     scanf("%d" , &a[i]);
      b[i] = a[i] - a[i - 1];
  }
  
  LL p =0 , q = 0;
  for (int i = 2; i <= n; i++) {
     if(b[i] > 0)  p += b[i];
     else q -= b[i];
  }
  cout << max(p , q) << endl;
  cout << abs(p - q) + 1 << endl;
  
  return 0;
}

102.最佳牛围栏

思路:前缀和+二分+平均值

经验:一般最大平均值->转化成二分问题。

解释:假设答案为ans ,每次check的平均数为avg , 当avg<=ans 时,总可以找到一段长度不小于F的区间使得其平均值>=avg;而当avg>ans时,找不到这样的区间。具有二段性,因此可以二分。

求平均值时,我们又可以把每个数都减去avg,便只用判断其总和的正负即可

判断一段数的和又可以用到前缀和的方法:q[r] - q[l - 1]。

想一次循环跑完,便用到双指针法。

要想总和最大,每个循环里把r固定,只需q[l - 1]最小即可,因此记录下全过程q[l - 1]的最小值,再找q[r]的最大值。

取minv时如果q[i1] < q[i2],即使i++,也相当于取的是i1~j+1这个区间,长度大于了F。

我们要寻找一段数列,这个数列满足,长度不小于L,并且它的子段和非负。也就是我们需要的二分判定。
二分:首先我们的mid=(l+r)/2,这里不能右移运算,浮点数除法。然后进行前缀和运算,s[i] = s[i - 1] + a[i] - mid, 这里要找的最优解[l , r] , a[l - 1] 尽可能小,a[r] 尽可能大。
image-20241111214616133

二分最难的地方就在于check函数的写法,我们先来捋一遍思路,防止写代码的时候思路混乱

①:我们要找的是 有没有一段不小于F的区间,使这段区间的平均数尽可能的大,如果我们找到了一段连续的区间且区间长度不小于F且平均数大于我们二分的平均数 那么大于这个数且区间也满足的一定满足了 我们直接判断正确即可

②:根据平均数的一个基本应用,每个数减去我们所算的平均数,如果大于0 ,那么他本身就大于平均数再使用前缀和,如果s[r] - s[l -1] >=0那么说明区间里的数满足条件。因此需要s[r]尽可能大,s[l -1]尽可能小。

③:据②我们还可以继续优化,因为我们不仅需要找F大小区间内,我们还要找>F大小区间内的,我们如果用二次for太费时间了,我们这里可以使用双指针的做法,我们设i=0,j=F 每次使两个数++ 因为i,j始终满足相距F的距离,所以我们用一个变量minv来存储sum[i]所遍历到的最小值,这样我们比较的距离一定是≥F的,此时若用sum[j]去减去minv的话,就能得到我们的最优解,如果这个最优解>= 0 那么就满足我们的指定条件。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;
const int N = 1e5 + 10;
double a[N] , s[N];
int n ,f;

bool check(double avg){
  for (int i = 1; i <= n; i++) {
     s[i] = s[i - 1] + a[i] - avg;
  }
  double minv = 0;
  for (int i = 0 , j = f; j <= n; i ++ , j++) {
     minv = min(minv , s[i]);
     if(s[j] - minv >= 0)  return true;
  }
  return false;
}

int main(){

  scanf("%d%d" , &n , &f);
  double l = 0 , r = 0;
  for (int i = 1; i <= n; i++)  {
    cin >> a[i]; 
    r = max(r , a[i]);//区间最大值就是数组最大值
  }

  while(r - l > 1e-5) //题目要求保留三位小数,一般e的指数绝对值取保留位数+2即可
  {
    double mid = (l + r) / 2;
    if(check(mid)) l = mid;//check()==true说明avg<ans,更新左区间
    else r = mid;  
  }
  cout << int(r * 1000) ;
  return 0;
}

11/12补充

113.特殊排序

思路:二分

本题与一般排序有三个区别:

  1. 是交互式,你并不知道大小关系,只能通过调用compare接口询问;

  2. 是大小不具备传递性,比如a < b,b < c 并不能推出a < c;

  3. 不能超过一万次询问,数据范围为1000,nlogn略小于一万,而CBA算法在最坏情况下的下界也就是nlogn。

    对于其性质2仅仅导致答案不唯一,题目仅要求输出一种答案,所以可以忽视该条件。

    采用二分插入排序解决该问题,首先将第一个元素压入向量里,然后二分查找合适的位置r,将待插入元素插入到向量末尾,从后往前不断交换相邻的两个数直到待插入的元素到达指定位置r。

    注意该二分算法的写法,循环退出时l = r + 1,意味着r位置的必然小于待插入的元素,r+1及其之后的元素都大于待插入的元素。(r返回的是小于待插入元素x的最大元素的位置,r + 1位置的元素是大于x的,目标就是将x放在r位置的后面)

    (比yxc大佬的代码更加简练了一点,因为我觉得mid=1+r+1>>2以致于代码最后还要多一次判断不容易理解,不如直接在循环里就判断好了,后面插入到末尾后只用不断前移,不用再进行判断了)。

class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int> res(1,1);
        for(int i = 2;i <= N;i++){
            int l = 0,r = res.size() - 1;
            while(l <= r){
                int mid = l + r >> 1;
                if(compare(res[mid],i)) l = mid + 1;
                else    r = mid - 1;
            }
            res.push_back(i);
            for(int j = res.size() - 2;j > r;j--)   swap(res[j],res[j + 1]);
        }
        return res;
    }
};

posted @ 2024-11-11 22:04  七龙猪  阅读(2)  评论(0)    收藏  举报
-->