2017-5-14 省賽模板

  • 簡介
  • 搜索
    • 迷宮(BFS+隊列)
  • 最短路
    • Dijkstra+鄰接矩陣
    • Dijkstra+鏈式前向星+優先隊列
    • Bellman-Ford + 鏈式前向星
    • Bellman-Ford(標準)
    • Floyd
    • SPFA+鄰接矩陣
    • SPFA+DFS
    • 最短路例題
  • 生成樹
    • Kruskal
    • Prim
    • 次小生成樹例題
  • 并查集
    • 一般并查集
    • 帶權并查集
    • 并查集例題
  • 線段樹
    • 漂浮法
    • 線段樹例題
  • 動態規劃
    • 最長公共子序列
    • 最長非降子序列(樸素)
    • 最長非降子序列(二分)
    • 最大字段和
    • 遞推例題
    • 01背包
    • 初始化的細節問題
    • 完全背包問題
    • 多重背包問題
    • 數位DP例題
    • 狀壓DP
    • 樹形DP
  • 數學
    • GCD與LCM
    • 簡易擴展歐幾里得算法
  • 雜項模板
    • 萬金油小算法
    • STL用法
    • 其他

簡介

自用模板。

盡量選用題目來對模板進行解釋。

搜索

迷宮(BFS+隊列)

用結構體作為隊列元素,三維數組存儲地圖。找到起點,入隊,遍歷前后左右上下移動,分別入隊。用vis數組進行判重。

#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;

struct node{
int x, y, z;
int step;
};
int sx, sy, sz; // 儲存起始點坐標
int to[6][3]{1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1}; // 移動方向
int L, R, C; // 長寬高
char map[30][30][30];
int cnt, vis[30][30][30];

int check(int x, int y, int z){ // check it is illegal or not
if(x<0 || x>=L || y<0 || y>=R || z<0 || z>=C) return 1;
if(vis[x][y][z] || map[x][y][z] == '#') return 1;
return 0;
}

void bfs(){
queue<node> q;
node a, b;
a.x = sx, a.y = sy, a.z = sz;
a.step = 0;
vis[a.x][a.y][a.z] = 1;
q.push(a);
while(!q.empty()){
a = q.front(); q.pop();
if(map[a.x][a.y][a.z] == 'E'){ // 遇到終點結束
cnt = a.step;
return;
}
for(int i=0; i<6; i++){ // 前后左右上下移動
b = a;
b.x += to[i][0];
b.y += to[i][1];
b.z += to[i][2];
if(check(b.x, b.y, b.z)) continue;
b.step = a.step + 1;
vis[b.x][b.y][b.z] = 1;
q.push(b);
}
}
cnt = -1;
}

int main(){
while(scanf("%d%d%d", &L, &R, &C), L){
for(int i=0; i<L; i++)
for(int j=0; j<R; j++)
scanf("%s", map[i][j]); // input
for(int i=0; i<L; i++)
for(int j=0; j<R; j++)
for(int k=0; k<C; k++)
if(map[i][j][k] == 'S'){
sx = i; sy = j; sz = k; // find start
}
memset(vis, 0, sizeof(vis)); // initial
bfs();
if(cnt == -1) { printf("Trapped!\n"); } // output
else printf("Escaped in %d minute(s).\n", cnt);
}
return 0;
}

最短路

Dijkstra+鄰接矩陣

鄰接矩陣設置為全局變量,無向邊存儲兩次。結果是起點到所有點的最短距離數組,不能判負環。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1000;

int map[maxn][maxn];
int pre[maxn], dis[maxn];
bool vis[maxn];
int n,m;

void Dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
memset(pre,-1,sizeof(pre));
memset(vis,false,sizeof(vis));
for(int i=1;i<=n;++i){
dis[i]=map[s][i];
pre[i]=s;
}
dis[s]=0;
vis[s]=true;
for(int i=2;i<=n;++i){
int mindist=INF;
int u=s;
for(int j=1;j<=n;++j)
if((!vis[j])&&dis[j]<mindist){
u=j; mindist=dis[j];
}
vis[u] = true;
for(int j=1;j<=n;++j)
if((!vis[j]) && map[u][j] < INF){
if(map[u][j] + dis[u] < dis[j]){
dis[j] = map[u][j] + dis[u];
pre[j] = u;
}
}
}
}

Dijkstra+鏈式前向星+優先隊列

該模板需要初始化。
通過addnode方法加邊,無向邊讀取兩遍。優先隊列優化時間效率,鏈式前向星遍歷某節點下所有邊,同時優化空間效率。結果是一個從起點到所有點的最短距離數組。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <queue>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=1000;

struct node{
int d,u;
friend bool operator<(node a,node b){
return a.d>b.d;
}
node(int dist,int point):d(dist),u(point){}
};

struct Edge{
int to,next;
int dist;
}edge[maxm];
int head[maxn],tot;
int pre[maxn],dis[maxn];

void init(){
memset(head,-1,sizeof(head));
tot=0;
}

void addedge(int u,int v,int d){
edge[tot].to=v;
edge[tot].dist=d;
edge[tot].next=head[u];
head[u]=tot++;
}

void Dijkstra(int s){
priority_queue<node> q;
memset(dis,0x3f,sizeof(dis));
memset(pre,-1,sizeof(pre));
dis[s]=0;
while(!q.empty()) q.pop();
node a(0,s); q.push(a);       //起點入隊列
while(!q.empty()){
node x=q.top(); q.pop();
if(dis[x.u]<x.d) continue;
for(int i=head[x.u];i!=-1;i=edge[i].next){
int v=edge[i].to;
if(dis[v]>dis[x.u]+edge[i].dist){
dis[v]=dis[x.u]+edge[i].dist;
pre[v]=x.u;
q.push(node(dis[v],v));
}
}
}
}

Bellman-Ford + 鏈式前向星

我個人感覺這個算法的優化在于重邊,以及松弛函數與主算法的分離。某些只針對松弛的題目變化可以很方便地使用。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
struct Edge{
int u, v;
double r, c;
}edge[maxn*2];

double mostMoney[maxn];
int n, m, s;
double v;
int tot;

void addedge(int u, int v, double r, double c){
edge[tot].u = u;
edge[tot].v = v;
edge[tot].r = r;
edge[tot++].c = c;
}

bool relax(int n){
double temp = (mostMoney[edge[n].u] - edge[n].c)*edge[n].r;
if (temp > mostMoney[edge[n].v]){
mostMoney[edge[n].v] = temp;
return true;
}
return false;
}

bool bellman_ford(){
bool flag;
for (int i=0; i<n; i++) mostMoney[i] = 0.0;
mostMoney[s] = v;
for (int i=0; i<n-1; ++i){
flag = false;
for (int j=0; j<tot; ++j)
if (relax(j)) flag = true;
if (mostMoney[s] > v) return true;
if (!flag) return false;
}
for (int i=0; i<tot; ++i)
if (relax(i)) return true;
return false;
}

Bellman-Ford(標準)

用簡單的結構體來存儲數據、遍歷。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=1000;

struct Edge{
int u,v; //起點、終點
int dist; //長度
}edge[maxn];

int dis[maxn]; //最短距離數組
int n, m; //結點數、邊數

bool Bellman_ford(int s){
memset(dis, INF, sizeof(dis));
dis[s]=0;
for(int k=1; k<n; ++k){ //迭代n-1次
for(int i=0; i<m; ++i){  //檢查每條邊
int x = edge[i].u, y = edge[i].v;
if(dis[x] < INF)
dis[y] = min(dis[y], dis[x] + edge[i].dist);
}
}
bool flag=1;
for(int i=0; i<m; ++i){   //判斷是否有負環
int x = edge[i].u, y = edge[i].v;
if(d[y] > d[x] + edge[i].dist){
flag = 0; break;
}
}
return flag;
}

Floyd

此算法三重循環是有順序的。

最短路松弛算法為:
map[i][j] = min(map[i][k]+map[k][j], map[i][j])

最外層循環為中繼點,第二次為起點,第三層為終點。

void floyd(){
for(int k=1; k<=n; ++k)
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
if (map[i][j] > map[i][k] + map[k][j]) //松弛
map[i][j] = map[i][k] + map[k][j];
}

SPFA+鄰接矩陣

void spfa(int s){
for(int i=0; i<=n; i++) dis[i]=0x3f3f3f3f; //初始化每點到s的距離
dis[s]=0; vis[s]=1; q[1]=s;  //隊列初始化,s為起點
int i, v, head=0, tail=1;
while (head++<tail){   //隊列非空
v=q[head];  //取隊首元素
vis[v]=0;   //釋放隊首結點,因為這節點可能下次用來松弛其它節點,重新入隊
for(i=0; i<=n; i++)  //對所有頂點
if (a[v][i]>0 && dis[i]>dis[v]+a[v][i]){
dis[i] = dis[v]+a[v][i];   //修改最短路
if (vis[i]==0){  //如果擴展結點i不在隊列中,入隊
q[++tail]=i;
vis[i]=1;
}
}
}
}

SPFA+DFS

a[i][j]存儲從ij所需要的距離。

b[i][j]=w存儲從iwi結點下的第j條邊。

void spfa(int s){
for(int i=1; i<=b[s][0]; i++)  //b[s,0]是從頂點s發出的邊的條數
if (dis[b[s][i]]>dis[s]+a[s][b[s][i]]){  //b[s,i]是從s發出的第i條邊的另一個頂點
dis[b[s][i]=dis[s]+a[s][b[s][i]];
spfa(b[s][i]);
}
}

最短路例題

CSU - 1808

題意

最短路。

給出n個地鐵站,m條邊,給出每條邊的首尾地鐵站a、b,和這條邊所屬的地鐵線c,以及這條邊的邊權d

地鐵線之間需要換乘,換乘時間為abs(ci-cj)
因為多了一個換乘時間,所以需要拆點。

用鏈式前向星存邊,用map<int,int>拆點,用vector存當前點所在的地鐵。

思路

首先讀邊。用map[u][x]來表示u地鐵站在x號線上,存儲一個標記值cnt,代表這個點是第幾個點(重新編號)。然后取出這個點。v點同樣。最后addedge兩遍。

遍歷所有點。對每個點下的vector排個序,這樣就可以避免取絕對值。對每個點遍歷vector,對于vector中的每條地鐵線,將這個點取出添邊。

制圖之后就按照模板跑Dijkstra,注意最后獲取結果的時候要跑一遍n點的vector找最小值。

代碼

/******************************
*File name: csu1808.cpp
*Author: wzhzzmzzy
*Created Time: 一  4/17 20:54:29 2017
*TODO: CSU 1808 最短路
******************************/

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int maxn = 300005;
const int inf = 0x3f3f3f3f;

int n, m;
struct Edge{
int to, next;
int w;
}edge[maxn<<1];

int head[maxn], tot;

void init(){
memset(head, -1, sizeof(head));
tot = 0;
}

void addedge(int u, int v, int w){
edge[tot].to = v;
edge[tot].w = w;
edge[tot].next = head[u];
head[u] = tot++;
}

vector<int> num[maxn];
//某站所屬的線路
map<int,int> mmp[maxn];
//某站是記錄的第幾個地鐵站(拆點)
int dis[maxn], cnt;

struct node{
int now, c;
node(int _n=0, int _c=0):now(_n), c(_c){}
friend bool operator <(node a, node r){
return a.c > r.c;
}
};

void Dijkstra(){
priority_queue<node> q;
while (!q.empty()) q.pop();
for (int i = 1; i < cnt; ++i) dis[i] = inf;
for (int i = 0; i < num[1].size(); ++i){
int st = mmp[1][num[1][i]];
dis[st] = 0;
q.push(node(st, 0));
}
node temp;
while (!q.empty()){
temp = q.top(); q.pop();
int u = temp.now;
int cost = temp.c;
if (cost > dis[u]) continue;
for (int i = head[u]; ~i; i=edge[i].next){
int v = edge[i].to;
int w = edge[i].w;
if (dis[v] > cost+w){
dis[v] = cost + w;
q.push(node(v, dis[v]));
}
}
}
}

int main(){
int u, v, x, w;
while (~scanf("%d%d", &n, &m)){
init();
cnt = 1;
for (int i = 1; i <= n; ++i){
num[i].clear();
mmp[i].clear();
}
for (int i = 0; i < m; ++i){
scanf("%d%d%d%d", &u, &v, &x, &w);
if (!mmp[u][x]){
mmp[u][x] = cnt++;
num[u].push_back(x);
}
u = mmp[u][x];
if (!mmp[v][x]){
mmp[v][x] = cnt++;
num[v].push_back(x);
}
v = mmp[v][x];
addedge(u, v, w);
addedge(v, u, w);
}
for(int i = 1; i <= n; ++i){
sort(num[i].begin(), num[i].end());
for(int j = 0; j < num[i].size()-1; ++j){
u = mmp[i][num[i][j]];
v = mmp[i][num[i][j+1]];
w = num[i][j+1] - num[i][j]; //同一站點不同線路的拆點之間的差值
addedge(u, v, w);
addedge(v, u, w);
}
}
Dijkstra();
int ans = inf;
for (int i = 0; i < num[n].size(); ++i){
u = mmp[n][num[n][i]];
ans = min(ans, dis[u]);
}
printf("%d\n", ans);
}
return 0;
}

生成樹

Kruskal

用并查集來將所有的節

點組織成一棵樹。不需要正反讀無向邊,默認無向。

#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn;
int fa[maxn];
int n, m;

struct Edge{
int u, v, w;
}edge[maxn];

bool cmp(Edge a,Edge b){
return a.w < b.w;
}

int find(int x){
return fa[x] == x? x: fa[x] = find(fa[x]);
}

int Kruskal(){
int ans = 0;
for(int i=0; i<=n; ++i)
fa[i]=i;
sort(edge, edge+m, cmp);
for(int i = 0; i < m; ++i){
int x = find(edge[i].u);
int y = find(edge[i].v);
if(x != y){
ans += edge[i].w;
fa[x] = y;
}
}
return ans;
}

Prim

void prim(){
int mi, v = 0;
for(int i = 0; i < n; i++){
dis[i] = map[0][i];
vis[i] = false;
}

for(int i = 1; i <= n; i++){
mi = INF;
for(int j = 0; j < n; j++)
if(!vis[j] && mi > dis[j]){
v = j;
mi = dis[j];
}
vis[v] = true;
for(int j = 0; j < n; j++)
if(!vis[j] && dis[j] > map[v][j])
dis[j] = map[v][j];
}
for(int i = 1; i < n; i++)
dis[0] += dis[i]; //統計生成樹長度
}

次小生成樹例題

POJ-1679

題意

套用kuangbin的次小生成樹模板。

只要找到次小生成樹,然后和最小生成樹比較一下即可。

代碼

/******************************
*File name: poj1679.cpp
*Author: wzhzzmzzy
*Created Time: 五  4/21 17:18:59 2017
*TODO: POJ 1679 次小生成樹
******************************/

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>

using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 105;

bool vis[maxn], used[maxn][maxn];
int lowc[maxn], pre[maxn], Max[maxn][maxn];

int prim(int cost[][maxn], int n){
int ans = 0;
memset(Max, 0, sizeof Max);
memset(used, false, sizeof used);
memset(vis, false, sizeof vis);
vis[0] = true;
pre[0] = -1;
for (int i = 1; i < n; ++i){
lowc[i] = cost[0][i];
pre[i] = 0;
}
lowc[0] = 0;
for (int i = 1; i < n; ++i){
int minc = inf;
int p = -1;
for (int j = 0; j < n; ++j) if (!vis[j] && minc > lowc[j]){
minc = lowc[j];
p = j;
}
if (minc == inf) return -1;
ans += minc;
vis[p] = true;
used[p][pre[p]] = used[pre[p]][p] = true;
for (int j = 0; j < n; ++j){
if (vis[j]) Max[j][p] = Max[p][j] = max(Max[j][pre[p]], lowc[p]);
if (!vis[j] && lowc[j] > cost[p][j]){
lowc[j] = cost[p][j];
pre[j] = p;
}
}
}
return ans;
}

int ans;
int check(int cost[][maxn], int n){
int Min = inf;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
if (cost[i][j] != inf && !used[i][j])
Min = min(Min, ans + cost[i][j] - Max[i][j]);
if (Min == inf) return -1;
return Min;
}

void solve(){
int cost[maxn][maxn], n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j){
if (i == j) cost[i][j] = 0;
else cost[i][j] = inf;
}
while (m--){
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
--x, --y;
cost[x][y] = cost[y][x] = w;
}
ans = prim(cost, n);
if (ans == -1 || ans == check(cost, n)) printf("Not Unique!\n");
else printf("%d\n", ans);
}

int main(){
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}

并查集

一般并查集

int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void join(int x, int y) {
int fx = find(x), fy = find(y);
if (fx != fy) fa[fx] = fy;
}

帶權并查集

而帶權并查集,其中的權需要轉化成子節點與父節點之間的聯系。這樣向上查找時就能發現父節點和子節點之間的關系,以此來進行計算。

帶權并查集的壓縮路徑方法是,在遞歸向上查找的同時,因為遞歸是直接到達最深處然后向上回溯的,所以只需要對每個點都做一次累加,這樣回到原來的位置時就是全部的累加。合并樹操作各有不同,主要是創建父節點的操作。

舉個例子,蟲子的一生( POJ - 2492 )中用權數組表示兩個蟲子的性別關系,更新時就只要考慮一下同性還是異性即可。

并查集例題

POJ - 1733

題解

并查集+離散化。
有一個01串,1e9位。給出一些子串,和他們中有奇數個還是偶數個1,求這些中有幾個是對的。
因為長度太長所以開不下這么多數組,而子串只有五千個,所以需要哈希一下。只要存最多一萬個數字就可以。為了避免不在一起的相鄰所以多存一位,就是兩萬位。然后用一個數組存當前下標之前有奇數還是偶數個1,最后用并查集求解。

代碼

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 100005;

int fa[maxn], val[maxn], n, m;
int hashSet[maxn];

struct {
int u, v, w;
}node[maxn];

int find(int n){
int k = fa[n];
if(fa[n] != n){
fa[n] = find(fa[n]);
val[n] = (val[n] + val[k])%2;
}
return fa[n];
}

void init(){
for (int i = 0; i <= n; ++i)
val[i] = 0, fa[i] = i;
}

int main(){
while (~scanf("%d", &n)){
int i, k = 0;

//init放在這里會RE

scanf("%d", &m);
for (i = 0; i < m; ++i){
char s[5];
scanf("%d%d%s", &node[i].u, &node[i].v, s);
node[i].w = s[0] == 'e'? 0:1;

hashSet[k++] = node[i].u - 1;
hashSet[k++] = node[i].u;
hashSet[k++] = node[i].v - 1;
hashSet[k++] = node[i].v;
}
hashSet[k++] = n;
hashSet[k++] = n - 1;

sort(hashSet, hashSet+k);
n = (int)(unique(hashSet, hashSet+k) - hashSet);

init();
//init放這里就AC

for (i = 0; i < m; ++i){
int u = (int)(lower_bound(hashSet, hashSet+n, node[i].u-1) - hashSet);
int v = (int)(lower_bound(hashSet, hashSet+n, node[i].v) - hashSet);

int fu = find(u), fv = find(v);

if (fu == fv && (val[u] + node[i].w)%2 != val[v])
break;
if (fu < fv){
fa[fv] = fu;
val[fv] = (val[u] + node[i].w - val[v] + 2) % 2;
}
if (fu > fv){
fa[fu] = fv;
val[fu] = (val[v] - node[i].w - val[u] + 2) % 2;
}
}
printf("%d\n", i);
}
return 0;
}

線段樹

漂浮法

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <queue>
#include <string>
using namespace std;

int t, n, a, b;
#define maxn  10001
int l[maxn], l[maxn];
int ans[maxn];

void cover(int a, int b, int to, int id) {
while (to <= n && (b < l[to] || a > r[to]))
to++;
if (to == n+1) {
ans[id] = b - a + 1; return;
}
if (a < l[to])
cover(a, l[to] - 1, to + 1, id);
if (b > r[to])
cover(r[to] + 1, b, to + 1, id);
}

int main() {
scanf("%d", &t);
while (t--) {
memset(ans, 0, sizeof(ans));
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d %d", &l[i], &r[i]);
for (int i = n; i >= 1; i--)
cover(l[i], r[i], i + 1, i);
int cnt = 0;
for (int i = 1; i <= n; i++)
if (ans[i])cnt++;
printf("%d\n", cnt);
}
}

線段樹例題

POJ-3468

代碼

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <queue>
#include <string>
using namespace std;

#define maxn 100010<<2
#define maxm 1000000000

int n, m;
long long A[100010];
long long Sum[maxn];
long long Add[maxn];

void pushup(int rt) {
Sum[rt] = Sum[rt << 1] + Sum[rt << 1 | 1];
}
void build(int l,int r,int rt) {
if (l == r) { Sum[rt] = A[l]; return; }
int m = (l + r) >> 1;
build(l, m, rt << 1);
build(m + 1, r, rt << 1 | 1);
pushup(rt);
}
void pushdown(int rt,int ln,int rn) {
if (Add[rt]) {
Add[rt << 1] += Add[rt];
Add[rt << 1 | 1] += Add[rt];
Sum[rt << 1] += Add[rt] * ln;
Sum[rt << 1 | 1] += Add[rt] * rn;
Add[rt] = 0;
}
}
void update(int a, int b, int c, int l, int r, int rt) {
if (a <= l&&b >= r) {
Sum[rt] += c*(r - l + 1);
Add[rt] += c;//本區間正確,子區間Sum仍然要用Add增加
return;
}
int m = (l + r) >> 1;
pushdown(rt, m - l + 1, r - m);
if (a <= m)update(a, b, c, l, m, rt << 1);
if (b > m)update(a, b, c, m + 1, r, rt << 1 | 1);
pushup(rt);
}
long long query(int a, int b, int l, int r, int rt) {
if (a <= l&&b >= r) { return Sum[rt]; }
int m = (l + r) >> 1;
pushdown(rt, m - l + 1, r - m);
long long ans = 0;
if (a <= m)ans += query(a, b, l, m, rt << 1);
if (b > m)ans += query(a, b, m + 1, r, rt << 1 | 1);
return ans;
}
int main() {
while (~scanf("%d %d", &n, &m)) {
memset(Add, 0, sizeof(Add));
for (int i = 1; i <= n; i++) {
scanf("%lld", &A[i]);
}
build(1, n, 1);
char s[10];
int a, b, c;
for (int i = 1; i <= m; i++) {
scanf("%s", s);
if (s[0] == 'Q') {
scanf("%d %d", &a, &b);
printf("%lld\n",query(a, b, 1, n, 1));
}
else {
scanf("%d %d %d", &a, &b, &c);
update(a, b, c, 1, n, 1);
}
}
}
}

動態規劃

最長公共子序列

if (a[i] == b[j])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);

最長非降子序列(樸素)

dp[i]存儲a[i]位置及之前最長非降子序列長度。

for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[i] >= a[j])
dp[i] = max(dp[i], dp[j]+1);

最長非降子序列(二分)

for (int i = 1; i <= n; ++i){
int k = upper_bound(a[i]);
b[k+1] = a[i];
}

最大字段和

int f(int n){
bool flag = false;
for (int i = 1; i <= n; ++i){
if (!flag && a[i] > 0) flag = true;
if (dp[i-1] + a[i] < 0) dp[i] = 0;
else dp[i] = dp[i-1] + a[i];
}
if (!flag){
int ans = 0;
for (int i = 1; i <= n; ++i)
ans += a[i];
return ans;
}
else return dp[n];
}

遞推例題

POJ - 1661

題意

Jimmy從坐標為(x,y)的點掉落下來,速度1單位/s,如果掉落下去的距離超過了max就會摔死。空間中有許多橫著的木板,屬性為(x1,x2,h)。在木板上可以橫向移動,速度1單位/s。求最快到達地面的時間。

題解

基礎DP。

dp[i][0]表示從第i塊木板左側掉落的到達地面時間,dp[i][1]表示右側。轉移方程為:

dp[i][0] =
p[i].h - p[k].h +
min(dp[k][0]+p[i].x1-p[k].x1,
dp[k][1]+p[k].x2-p[i].x1);
dp[i][1] =
p[i].h - p[k].h +
min(dp[k][0]+p[i].x2-p[k].x1,
dp[k][1]+p[k].x2-p[i].x2);

代碼

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <iostream>
using namespace std;

struct plat{
int x1, x2, h;
}p[1005];
int maxn, dp[1005][2];

bool cmp(plat a, plat b){
return a.h < b.h;
}

void calc_left(int i){
int k = i - 1;
while (k > 0 && p[i].h - p[k].h <= maxn){
if (p[i].x1 >= p[k].x1 && p[i].x1 <= p[k].x2){
dp[i][0] = p[i].h - p[k].h + min(dp[k][0]+p[i].x1-p[k].x1,
dp[k][1]+p[k].x2-p[i].x1);
return;
} else --k;
}
if (p[i].h - p[k].h > maxn)
dp[i][0] = 0x3f3f3f3f;
else
dp[i][0] = p[i].h;
}

void calc_right(int i){
int k = i - 1;
while (k > 0 && p[i].h - p[k].h <= maxn){
if (p[i].x2 >= p[k].x1 && p[i].x2 <= p[k].x2){
dp[i][1] = p[i].h - p[k].h + min(dp[k][0]+p[i].x2-p[k].x1,
dp[k][1]+p[k].x2-p[i].x2);
return;
} else --k;
}
if (p[i].h - p[k].h > maxn)
dp[i][1] = 0x3f3f3f3f;
else
dp[i][1] = p[i].h;
}

void solve(){
memset(dp, 0, sizeof dp);
int n;
scanf("%d%d%d%d", &n, &p[0].x1, &p[0].h, &maxn);
++n, p[0].x2 = p[0].x1;
for (int i = 1; i < n; ++i)
scanf("%d%d%d", &p[i].x1, &p[i].x2, &p[i].h);
p[n].x1 = -20001, p[n].x2 = 20001, p[n].h = 0;
sort(p, p+n+1, cmp);

for (int i = 1; i <= n; ++i){
calc_left(i);
calc_right(i);
}

printf("%d\n", min(dp[n][0], dp[n][1]));
}

int main(){
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}

01背包

每個物品只能放入一次。

思路

f[i][v]表示,第i個大小為v的物品放入時的總價值。

c[i]表示第i個物品的價值。w[i]為第i個物品的大小。

狀態轉移方程:f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+c[i]);

狀態轉移方程表示,取放入或者不放入第i個物品兩種情況的最大值。

空間優化(滾動數組)

初始狀態方程的空間復雜度是O(V*W),可以進一步優化。

可以將空間優化為O(2*W),即縱向大小為2。

for(i=1; i<=N; i++){
for(j=t[i]; j<=V; j++)
f[t^1][j] = max(f[c][j-w[i]]+c[i], f[t][j]);
t ^= 1;
}

異或滾動可以在0和1之間切換,可以利用上下反復更新。

空間優化(一維數組)

既然可以用兩行進行更新,那為什么不能用一行。

觀察問題,兩行更新時,用上一行的前部分更新下一行的后部分。

所以單行更新時要從后往前遍歷,這樣可以用前面的更新后面的。

for(i=1; i<=N; i++)
for(j=V; j>=w[i]; j--)
f[j] = max(f[j-w[i]]+c[i], f[j]);

這樣就可以用一維數組來進行更新。

可以寫成函數,封裝起來。

void ZeroOnePack(int cost, int weight){
for(int i=V; i>=weight; i++)
f[i] = max(f[i], f[i-weight]+cost)
}

初始化的細節問題

一般問題會有兩種問法:

  1. 剛好裝滿背包
  2. 不用裝滿背包

如果是第一種,f[0]=0,f[1]……f[N]=INF;

如果是第二種,f[0]……f[N]=INF;

理解:

如果是第一種,初始狀態只有0符合理想狀態,只有0才能被空“裝滿”。

如果是第二種,所有都符合理想狀態。

完全背包問題

和01背包相似,所不同的是可取的物品數是無限。

前置小優化

對于i``j兩個物品,如果c[i]>c[j] && w[i]<w[j],就舍去i物品。

另外,針對背包問題而言,比較不錯的一種方法是:首先將重量大于V的物品去掉,然后使用類似計數排序的做法,計算出費用相同的物品中價值最高的是哪個,可以O(V+N)地完成這個優化。

基本思路

狀態轉移方程f[i][v]=max{f[i-1][v-k*w[i]]+k*c[i]},(0<=k*w[i]<=V)

轉化為01背包求解

一件物品最多只能放V/c[i]件,所以可以把一件物品,看成V/c[i]件物品,作為01背包解答。

另一種更好的辦法是把第i種物品拆成大小為w[i]*2^k、價值為c[i]*2^k的若干件物品,其中k滿足w[i]*2^k<=V。這是二進制的思想,因為不管最優策略選幾件第i種物品,總可以表示成若干個2^k件物品的和。這樣把每種物品拆成O(log(V/w[i]))件物品,是一個很大的改進。

O(VN)算法

for(int i=1; i<=N; i++)
for(int j=w[i]; j<=V; j++)
f[j] = max{f[v], f[v-w[i]]+c[i]};

這個算法和之前的01背包相比只是第二層的遍歷方向改變了。因為01背包要保證每個物品只能選擇一次,但是完全背包不必,所以改變遍歷方向就可以得到結果。

這個算法也可以從另外的思路中得出,
例如,基本思路中的公式可以化作這個形式:
f[i][v]=max(f[i-1][v], f[i][v-w[i]]+c[i]);

用函數封裝:

void CompletePack(int cost, int weight){
for(int i=weight; i<=V; i++)
f[i] = max(f[i], f[i-weight]+cost);
}

多重背包問題

每件物品數量不一定為1但有限。

基本思路

問題和完全背包很相似。
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[I]}(0<=k<=n[I])
復雜度為O(V*Σn[i])

轉化為01背包問題

n[i]存儲,可以將每種物品轉化為n[i]件物品,然后用01背包方案求解。復雜度不變。

如果要進行優化的話,依然用二進制思想,同上。

這樣可以將時間優化為O(V*Σlog n[i])

void MultiplePack(int weight, int cost, int amount){
if(cost * amount >= V){
CompletePack(cost, weight);
return;
}
int k = 1;
while(k < num){// num 為物品種數
ZeroOnePack(k*cost, k*weight);
amount = amount-k;
k *= 2;
}
ZeroOnePack(amount*cost, amount*weight);
}

數位DP例題

HDU - 2089

題意

求從lr的所有數字中,不含有462的數字有多少。

題解

數位DP入門。

dp[i][j]表示j開頭的i位數。首先打表,然后根據讀入的數字挨個匹配累加即可。

遞推公式:

for (int i = 1; i < 7; ++i)
for (int j = 0; j < 10; ++j)
for (int k = 0; k < 10; ++k)
if (j != 4 && !(j == 6 && k == 2))
dp[i][j] += dp[i-1][k];

代碼

/******************************
*File name: hdu2089.cpp
*Author: wzhzzmzzy
*Created Time: 二  4/25 20:05:44 2017
*TODO: HDU 2089 數位DP
******************************/

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>

using namespace std;

int dp[8][10];
void init(){
memset(dp, 0, sizeof dp);
dp[0][0] = 1;
for (int i = 1; i < 7; ++i)
for (int j = 0; j < 10; ++j)
for (int k = 0; k < 10; ++k)
if (j != 4 && !(j == 6 && k == 2))
dp[i][j] += dp[i-1][k];
}

int calc(int x){
int digit[8], len = 0, ans = 0;
while (x > 0){
digit[++len] = x%10;
x /= 10;
}
digit[len+1] = 0;
for (int i = len; i; --i){
for (int j = 0; j < digit[i]; ++j)
if (j != 4 && !(digit[i+1]==6&&j==2))
ans += dp[i][j];
if (digit[i] == 4||(digit[i]==2&&digit[i+1]==6))
break;
}
return ans;
}

int main(){
int n, m;
init();
while (~scanf("%d%d", &n, &m) && n && m){
printf("%d\n", calc(m+1)-calc(n));
}
}

狀壓DP

HDU - 1074

題意

給出n門作業的名稱、截止時間和所需時間。當超過截止時間時會扣分。求最小的扣分數和做作業的順序。

題解

狀態壓縮DP。
看了題解才知道這樣做。
首先是二進制法遍歷集合全排列,然后遍歷當前排列中包含的上一個做完的作業,即遍歷每一種作業,找出扣分的最小值作為上一個做完的作業。記錄這一狀態,并且記錄前驅(輸出用)。

代碼

/******************************
*File name: hdu1029.cpp
*Author: wzhzzmzzy
*Created Time: 日  4/23 20:01:35 2017
*TODO: HDU 1029 狀壓DP 水
******************************/

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>

using namespace std;

char name[16][200];
int t[1<<15], deadline[16], finish[16];
int pre[1<<15], dp[1<<15];

void output(int m){
if (!m) return;
output(m-(1<<pre[m]));
printf("%s\n", name[pre[m]]);
}

int main(){
int T;
scanf("%d", &T);
while (T--){
int n;
scanf("%d", &n);
int maxn = 1<<n;
for (int i = 0; i < n; ++i)
scanf("%s%d%d", name[i], deadline+i, finish+i);
for (int i = 1; i < maxn; ++i){
dp[i] = 0x3f3f3f3f;
for (int j = n-1; j >= 0; --j){
int te = 1<<j;
if (!(i&te)) continue;
int score = t[i-te]-deadline[j]+finish[j];
if (score < 0) score = 0;
if (dp[i] > dp[i-te]+score){
dp[i] = dp[i-te]+score;
t[i] = t[i-te]+finish[j];
pre[i] = j;
}
}
}
printf("%d\n", dp[maxn-1]);
output(maxn-1);
}
return 0;
}

樹形DP

HDU - 4044

代碼

#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <queue>
#include <string>
#include <vector>
#include <set>
#include <math.h>
#include <map>
#define ull unsigned long long
#define ll long long
#define mp map
#define FOR(a,b) for(int i=a;i<=b;i++)
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
using namespace std;

int m;
const int maxm = 210;
const int maxn = 1010;

vector<int>aaa[maxn];

int weaponChoice[maxn];
int cost[maxn][52];
int power[maxn][52];
int dp[maxn][maxm];    //dp[i][j]:i與其子樹消耗j資源的最薄弱鏈最大值

void dfs(int u,int fa){
for(int i=m;i>=0;i--){
for(int j=1;j<=weaponChoice[u];j++){
if(cost[u][j]<=i)dp[u][i]=max(dp[u][i],power[u][j]);
}
}

if(aaa[u].size()==1&&u!=1) return;

int maxson[maxn];        //maxson[i]:u子樹花費資源i時最大值
memset(maxson,0x3f3f3f3f,sizeof(maxson));
for(int e=0;e<aaa[u].size();e++){//枚舉u的子節點
int v=aaa[u][e];
if(v==fa)continue;
dfs(v,u);                    //計算v樹的最優解,dp[v]系
for(int i=m;i>=0;i--){        //枚舉給u子樹分配的資源
int maxx=0;
for(int j=0;j<=i;j++){
maxx=max(maxx,min(maxson[i-j],dp[v][j]));    //其他子樹分i-j,v樹分j
}
maxson[i]=maxx;
}
}
for(int i=m;i>=0;--i)
for(int k=0;k<=i;++k)
dp[u][i]=max(dp[u][i],dp[u][i-k]+maxson[k]);
}
int main(){
int tcase;
scanf("%d",&tcase);
while(tcase--){
memset(dp,0,sizeof(dp));
int n;
scanf("%d",&n);
for(int i=0;i<=n;i++)aaa[i].clear();
int a,b;
for(int i=1;i<n;i++){
scanf("%d %d",&a,&b);
aaa[a].push_back(b);
aaa[b].push_back(a);
}

scanf("%d",&m);
for(int i=1;i<=n;i++){
scanf("%d",&weaponChoice[i]);
for(int j=1;j<=weaponChoice[i];j++){
scanf("%d %d",&a,&b);
cost[i][j]=a;power[i][j]=b;
}
}
dfs(1,-1);
printf("%d\n",dp[1][m]);
}
}

數學

GCD與LCM

  • GCD計算方式來自公式gcd(a,b)=gcd(b,a%b)
int gcd(int a, int b){
return b>0? b : a%b;
}
  • LCM計算方式來自性質ab=gcd(a,b)*lcm(a,b)
int lcm(int a, int b){
return a * b / std::__gcd(a, b);
}
  • 還有一個性質lcm(m*a,m*b)=m*gcd(a,b)

簡易擴展歐幾里得算法

void gcd(int a, int b, int& d, int& x, int& y) {
if (!b) { d = a; x = 1; y = 0; }
else { gcd(b, a%b, d, y, x); y -= x*(a / b); }
}

雜項模板

萬金油小算法

快讀

通過getchar和強制類型轉換加快讀取整形數字的速度。

int Input(){
char c;
for (c = getchar(); c<'0' || c>'9'; c = getchar());
int a = c - '0';
for (c = getchar(); c >= '0' && c <= '9'; c = getchar())
a = a*10 + c - '0';
return a;
}

交換

指針交換變量值。

void swap(int* a, int* b){
int t = *a; *a = *b; *b = t;
}

前驅路徑輸出

遞歸回溯輸出pre數組中的路徑。

void output(int cur, int pre[]){
if (cur == 0) return;
output(pre[cur], pre);
printf("%d\n", cur);
}

矩陣快速冪

#include <cstdio>
#include <algorithm>
#include <string.h>
#include <iostream>
using namespace std;
#define maxn 101    //題目極限尺寸

int n;              //實際矩陣尺寸
struct Matrix {
long long mat[maxn][maxn];
};

Matrix operator * (Matrix a, Matrix b) {
Matrix c;
memset(c.mat, 0, sizeof(c.mat));
int i, j, k;
for (k = 0; k < n; ++k) {
for (i = 0; i < n; ++i) {
for (j = 0; j < n; ++j) {
c.mat[i][j] += a.mat[i][k] * b.mat[k][j];
c.mat[i][j] %= 10000;
}
}
}
return c;
}

Matrix operator ^ (Matrix a, int k) {
Matrix c;
int i, j;
for (i = 0; i < n; ++i)
for (j = 0; j < n; ++j)
c.mat[i][j] = (i == j);    //初始化為單位矩陣

for (; k; k >>= 1) {
if (k & 1) c = c*a;
a = a*a;
}
return c;
}

int main() {
int t;
while (scanf("%d", &t) && t != -1) {
Matrix fib;
n = 2;
fib.mat[0][0] = fib.mat[0][1] = fib.mat[1][0] = 1;
fib.mat[1][1] = 0;
fib = fib^t;
cout << fib.mat[0][1]%10000 << endl;
}
}

STL用法

struct 重載運算符、構造函數

struct node{
int d,u;
friend bool operator<(node a, node b){
return a.d > b.d;
}
node():{}
node(int dist, int point): d(dist), u(point){}
};

std::sort()

按照a字段降序排序,a相同時按b降序。

bool cmp(node a, node b){
if (a.a == b.a)
return a.b > b.b;
else
return a.a > b.a;
}

std::map

map<string,int> a;
a["hello"] = 1;
a["world"] = 2;
for (map<string,int>::iterator i = a.begin(); i != a.end(); ++i)
cout << i->first << " " << i->second << endl;
a.count("world") == 2; // Ture;

auto 變量

C++11,auto為一塊指向無類型內存的變量。

int a = 0;
auto it = &a;
cout << *it << endl;

BigInteger

import java.math.BigInteger;
import java.util.*;

public class test {
public static void main(String args[]){
BigInteger
a = new BigInteger("32"),
m = new BigInteger("12");
BigDecimal b = new BigDecimal("1.0");
a = a.pow(1);
a = a.add(a);
a = a.divide(a);
a = a.mod(a);
a = a.abs();
a = a.gcd(a);
a = a.modPow(a, m);
System.out.print(a.toString()+"\n");
}
}

STL去重

sort(xs.begin(),sx.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end()); //unique將同樣的元素排至vector末位,返回首個重復元素地址

STL算法離散化

思路:先排序,再刪除重復元素,然后就是索引元素離散化后對應的值。

假定待離散化的序列為a[n],b[n]是序列a[n]的一個副本,則對應以上三步為:

sort(sub_a,sub_a+n);
int size=unique(sub_a,sub_a+n)-sub_a;//size為離散化后元素個數
for(i=0;i<n;i++)
a[i]=lower_bound(sub_a,sub_a+size,a[i])-sub_a + 1;//k為b[i]經離散化后對應的值

對于第3步,若離散化后序列為0, 1, 2, ..., size - 1則用lower_bound,從1, 2, 3, ..., size則用upper_bound,其中lower_bound返回第1個不小于b[i]的值的指針。

而upper_bound返回第1個大于b[i]的值的指針,當然在這個題中也可以用lower_bound然后再加1得到與upper_bound相同結果,兩者都是針對以排好序列。使用STL離散化大大減少了代碼量且結構相當清晰。

其他

Vim 配置

set nu
set mouse=a
set cursorline
set confirm
set tabstop=4
set smarttab
set shiftwidth=4
set smartindent
set cindent
set nobackup
set matchtime=1
set showmatch
set setnocompatible

colo evening

syntax on
syntax enable

G++ 運行

g++ a.cpp
./a.out
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容

  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,762評論 0 33
  • 樹形動態規劃,顧名思義就是樹+DP,先分別回顧一下基本內容吧:動態規劃:問題可以分解成若干相互聯系的階段,在每一個...
    Mr_chong閱讀 1,495評論 0 2
  • 一年級語文上冊生字表 生字表一(共400字) 啊(ā)愛(ài)安(ān)岸(àn)爸(bà)八(bā)巴(bā)...
    meychang閱讀 2,831評論 0 6
  • 總核數 = 物理CPU個數 X 每顆物理CPU的核數總邏輯CPU數 = 物理CPU個數 X 每顆物理CPU的核數 ...
    Cursor_fei閱讀 5,504評論 0 2
  • 追番神作不可缺少《海賊王》!小叮追了海賊王多年,不敢說集集都準時看,但到目前為止動畫和漫畫還是啃了好十幾年,所以今...
    愛叮叮閱讀 1,456評論 1 1