題目
有N種物品和一個容量為V的背包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。
基本思路
這個問題非常類似于01背包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已并非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01背包時的思路,令f[i][v]表示前i種物品恰放入一個容量為v的背包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
這跟01背包問題一樣有O(VN)個狀態需要求解,但求解每個狀態的時間已經不是常數了,求解狀態f[i][v]的時間是O(v/c[i]),總的復雜度可以認為是O(V*Σ(V/c[i])),是比較大的。
將01背包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明01背包問題的方程的確是很重要,可以推及其它類型的背包問題。但我們還是試圖改進這個復雜度。
一個簡單有效的優化
完全背包問題有一個很簡單有效的優化,是這樣的:若兩件物品i、j滿足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。這個優化的正確性顯然:任何情況下都可將價值小費用高得j換成物美價廉的i,得到至少不會更差的方案。對于隨機生成的數據,這個方法往往會大大減少物品的件數,從而加快速度。然而這個并不能改善最壞情況的復雜度,因為有可能特別設計的數據可以一件物品也去不掉。
這個優化可以簡單的O(N^2)地實現,一般都可以承受。另外,針對背包問題而言,比較不錯的一種方法是:首先將費用大于V的物品去掉,然后使用類似計數排序的做法,計算出費用相同的物品中價值最高的是哪個,可以O(V+N)地完成這個優化。這個不太重要的過程就不給出偽代碼了,希望你能獨立思考寫出偽代碼或程序。
轉化為01背包問題求解
既然01背包問題是最基本的背包問題,那么我們可以考慮把完全背包問題轉化為01背包問題來解。最簡單的想法是,考慮到第i種物品最多選V/c[i]件,于是可以把第i種物品轉化為V/c[i]件費用及價值均不變的物品,然后求解這個01背包問題。這樣完全沒有改進基本思路的時間復雜度,但這畢竟給了我們將完全背包問題轉化為01背包問題的思路:將一種物品拆成多件物品。
更高效的轉化方法是:把第i種物品拆成費用為c[i]*2^k、價值為w[i]*2^k的若干件物品,其中k滿足c[i]*2^k<=V
。這是二進制的思想,因為不管最優策略選幾件第i種物品,總可以表示成若干個2^k件物品的和。這樣把每種物品拆成O(log V/c[i])件物品,是一個很大的改進。
但我們有更優的O(VN)的算法。
O(VN)的算法
這個算法使用一維數組,先看偽代碼:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-cost]+weight}
你會發現,這個偽代碼與P01的偽代碼只有v的循環次序不同而已。為什么這樣一改就可行呢?首先想想為什么P01中要按照v=V..0的逆序來循環。這是因為要保證第i次循環中的狀態f[i][v]是由狀態f[i-1][v-c[i]]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第i件物品”這件策略時,依據的是一個絕無已經選入第i件物品的子結果f[i-1][v-c[i]]。而現在完全背包的特點恰是每種物品可選無限件,所以在考慮“加選一件第i種物品”這種策略時,卻正需要一個可能已選入第i種物品的子結果f[i][v-c[i]],所以就可以并且必須采用v=0..V的順序循環。這就是這個簡單的程序為何成立的道理。
值得一提的是,上面的偽代碼中兩層for循環的次序可以顛倒。這個結論有可能會帶來算法時間常數上的優化。
這個算法也可以以另外的思路得出。例如,將基本思路中求解f[i][v-c[i]]的狀態轉移方程顯式地寫出來,代入原方程中,會發現該方程可以等價地變形成這種形式:
f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}
將這個方程用一維數組實現,便得到了上面的偽代碼。
最后抽象出處理一件完全背包類物品的過程偽代碼:
procedure CompletePack(cost,weight)
for v=cost..V
f[v]=max{f[v],f[v-c[i]]+w[i]}
總結
完全背包問題也是一個相當基礎的背包問題,它有兩個狀態轉移方程,分別在“基本思路”以及“O(VN)的算法“的小節中給出。希望你能夠對這兩個狀態轉移方程都仔細地體會,不僅記住,也要弄明白它們是怎么得出來的,最好能夠自己想一種得到這些方程的方法。事實上,對每一道動態規劃題目都思考其方程的意義以及如何得來,是加深對動態規劃的理解、提高動態規劃功力的好方法。
posted @
2015-02-18 20:31 JulyRina 閱讀(379) |
評論 (0) |
編輯 收藏
題目
有N件物品和一個容量為V的背包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使價值總和最大。
基本思路
這是最基礎的背包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量為v的背包可以獲得的最大價值。則其狀態轉移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
這個方程非常重要,基本上所有跟背包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:“將前i件物品放入容量為v的背包中”這個子問題,若只考慮第i件物品的策略(放或不放),那么就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那么問題就轉化為“前i-1件物品放入容量為v的背包中”,價值為f[i-1][v];如果放第i件物品,那么問題就轉化為“前i-1件物品放入剩下的容量為v-c[i]的背包中”,此時能獲得的最大價值就是f[i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。
優化空間復雜度
以上方法的時間和空間復雜度均為O(VN),其中時間復雜度應該已經不能再優化了,但空間復雜度卻可以優化到O。
先考慮上面講的基本思路如何實現,肯定是有一個主循環i=1..N,每次算出來二維數組f[i][0..V]的所有值。那么,如果只用一個數組f[0..V],能不能保證第i次循環結束后f[v]中表示的就是我們定義的狀態f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]兩個子問題遞推而來,能否保證在推f[i][v]時(也即在第i次主循環中推f[v]時)能夠得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事實上,這要求在每次主循環中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c[i]]保存的是狀態f[i-1][v-c[i]]的值。偽代碼如下:
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相當于我們的轉移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]}
,因為現在的f[v-c[i]]就相當于原來的f[i-1][v-c[i]]。如果將v的循環順序從上面的逆序改成順序的話,那么則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它卻是另一個重要的背包問題P02最簡捷的解決方案,故學習只用一維數組解01背包問題是十分必要的。
事實上,使用一維數組解01背包的程序在后面會被多次用到,所以這里抽象出一個處理一件01背包中的物品過程,以后的代碼中直接調用不加說明。
過程ZeroOnePack,表示處理一件01背包中的物品,兩個參數cost、weight分別表明這件物品的費用和價值。
procedure ZeroOnePack(cost,weight)
for v=V..cost
f[v]=max{f[v],f[v-cost]+weight}
注意這個過程里的處理與前面給出的偽代碼有所不同。前面的示例程序寫成v=V..0是為了在程序中體現每個狀態都按照方程求解了,避免不必要的思維復雜度。而這里既然已經抽象成看作黑箱的過程了,就可以加入優化。費用為cost的物品不會影響狀態f[0..cost-1],這是顯然的。
有了這個過程以后,01背包問題的偽代碼就可以這樣寫:
for i=1..N
ZeroOnePack(c[i],w[i]);
初始化的細節問題
我們看到的求最優解的背包問題題目中,事實上有兩種不太相同的問法。有的題目要求“恰好裝滿背包”時的最優解,有的題目則并沒有要求必須把背包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。
如果是第一種問法,要求恰好裝滿背包,那么在初始化時除了f[0]為0其它f[1..V]均設為-∞,這樣就可以保證最終得到的f[N]是一種恰好裝滿背包的最優解。
如果并沒有要求必須把背包裝滿,而是只希望價格盡量大,初始化時應該將f[0..V]全部設為0。
為什么呢?可以這樣理解:初始化的f數組事實上就是在沒有任何物品可以放入背包時的合法狀態。如果要求背包恰好裝滿,那么此時只有容量為0的背包可能被價值為0的nothing“恰好裝滿”,其它容量的背包均沒有合法的解,屬于未定義的狀態,它們的值就都應該是-∞了。如果背包并非必須被裝滿,那么任何容量的背包都有一個合法解“什么都不裝”,這個解的價值為0,所以初始時狀態的值也就全部為0了。
這個小技巧完全可以推廣到其它類型的背包問題,后面也就不再對進行狀態轉移之前的初始化進行講解。
一個常數優化
前面的偽代碼中有 for v=V..1,可以將這個循環的下限進行改進。
由于只需要最后f[v]的值,倒推前一個物品,其實只要知道f[v-w[n]]即可。以此類推,對以第j個背包,其實只需要知道到f[v-sum{w[j..n]}]即可,即代碼中的
for i=1..N
for v=V..0
可以改成
for i=1..n
bound=max{V-sum{w[i..n]},c[i]}
for v=V..bound
這對于V比較大時是有用的。
小結
01背包問題是最基本的背包問題,它包含了背包問題中設計狀態、方程的最基本思想,另外,別的類型的背包問題往往也可以轉換成01背包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最后怎樣優化的空間復雜度。
posted @
2015-02-18 20:30 JulyRina 閱讀(275) |
評論 (0) |
編輯 收藏
題目大意:求圖上單點到單點之間的最短路。
題目分析:SPFA(Shortest Path Faster Algorithm)是Bellman-Ford算法的一種隊列實現,減少了不必要的冗余計算。
算法大致流程是用一個隊列來進行維護。 初始時將源加入隊列。 每次從隊列中取出一個元素,并對所有與他相鄰的點進行松弛,若某個相鄰的點松弛成功,則將其入隊。 直到隊列為空時算法結束。
這個算法,簡單的說就是隊列優化的bellman-ford,利用了每個點不會更新次數太多的特點發明的此算法
SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的時間復雜度內求出源點到其他所有點的最短路徑,可以處理負邊。SPFA的實現甚至比Dijkstra或者Bellman_Ford還要簡單:
設Dist代表S到I點的當前最短距離,Fa代表S到I的當前最短路徑中I點之前的一個點的編號。開始時Dist全部為+∞,只有Dist[S]=0,Fa全部為0。
維護一個隊列,里面存放所有需要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每個點是否處在隊列中。
每次迭代,取出隊頭的點v,依次枚舉從v出發的邊v->u,設邊的長度為len,判斷Dist[v]+len是否小于Dist[u],若小于則改進Dist[u],將Fa[u]記為v,并且由于S到u的最短距離變小了,有可能u可以改進其它的點,所以若u不在隊列中,就將它放入隊尾。這樣一直迭代下去直到隊列變空,也就是S到所有的最短距離都確定下來,結束算法。若一個點入隊次數超過n,則有負權環。
SPFA 在形式上和寬度優先搜索非常類似,不同的是寬度優先搜索中一個點出了隊列就不可能重新進入隊列,但是SPFA中一個點可能在出隊列之后再次被放入隊列,也就是一個點改進過其它的點之后,過了一段時間可能本身被改進,于是再次用來改進其它的點,這樣反復迭代下去。設一個點用來作為迭代點對其它點進行改進的平均次數為k,有辦法證明對于通常的情況,k在2左右。
SPFA算法(Shortest Path Faster Algorithm),也是求解單源最短路徑問題的一種算法,用來解決:給定一個加權有向圖G和源點s,對于圖G中的任意一點v,求從s到v的最短路徑。 SPFA算法是Bellman-Ford算法的一種隊列實現,減少了不必要的冗余計算,他的基本算法和Bellman-Ford一樣,并且用如下的方法改進: 1、第二步,不是枚舉所有節點,而是通過隊列來進行優化 設立一個先進先出的隊列用來保存待優化的結點,優化時每次取出隊首結點u,并且用u點當前的最短路徑估計值對離開u點所指向的結點v進行松弛操作,如果v點的最短路徑估計值有所調整,且v點不在當前的隊列中,就將v點放入隊尾。這樣不斷從隊列中取出結點來進行松弛操作,直至隊列空為止。 2、同時除了通過判斷隊列是否為空來結束循環,還可以通過下面的方法: 判斷有無負環:如果某個點進入隊列的次數超過V次則存在負環(SPFA無法處理帶負環的圖)。
SPFA算法有兩個優化算法 SLF 和 LLL: SLF:Small Label First 策略,設要加入的節點是j,隊首元素為i,若dist(j)<dist(i),則將j插入隊首,否則插入隊尾。 LLL:Large Label Last 策略,設隊首元素為i,隊列中所有dist值的平均值為x,若dist(i)>x則將i插入到隊尾,查找下一元素,直到找到某一i使得dist(i)<=x,則將i出對進行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高約 50%。 在實際的應用中SPFA的算法時間效率不是很穩定,為了避免最壞情況的出現,通常使用效率更加穩定的Dijkstra算法。
#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
#define INF (1<<29)
const int maxn = 1010;
typedef pair<int, int> P;
vector<P> G[maxn];
queue<int> que;
int V, E, dist[maxn];
bool vis[maxn];
void spfa(int s) {
fill(dist, dist+V, INF);
fill(vis, vis+V, false);
dist[s] = 0;
while(!que.empty()) que.pop();
que.push(s);
while(!que.empty()) {
int u = que.front();
que.pop();
vis[u] = false;
int sz = G[u].size();
for(int i=0;i<sz;i++) {
int v = G[u][i].first;
int w = G[u][i].second;
if(dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
if(!vis[v]) {
vis[v] = true;
que.push(v);
}
}
}
}
}
int main() {
scanf("%d%d" , &E, &V);
for(int i=0;i<V;i++) G[i].clear();
for(int i=0;i<E;i++) {
int u, v, w;
scanf("%d%d%d" , &u, &v, &w);
u --; v --;
G[u].push_back(make_pair(v, w));
G[v].push_back(make_pair(u, w));
}
spfa(0);
printf("%d\n", dist[V-1]);
return 0;
}
posted @
2015-02-13 19:42 JulyRina 閱讀(239) |
評論 (0) |
編輯 收藏
題目大意:求圖上單點到單點之間的最短路。
題目分析:之前我們在同一道問題上分析過了單源最短路問題的
Dijkstra算法。
使用鄰接矩陣實現的Dijkstra算法的時間復雜度是O(|V|^2)。使用鄰接表的話,更新最短距離只需要更新每一條邊即可,因此這部分的時間復雜度是O(|E|)。但是每次要枚舉所有的頂點來查找下一個使用的頂點,因此最終復雜度還是O(|V|^2)。在|E|比較小時,大部分經歷放在了查找下一次要是用的頂點上,因此需要使用合適的數據結構對其進行優化。
需要優化的是數值的插入(更新)和取出最小值兩個操作,因此使用堆就可以了。把每個頂點當前的最短距離用堆維護,在更新最短距離時,把對應的元素往根的方向移動以滿足堆的性質。而每次從堆中取出的最小值就是下一次要使用的頂點。這樣堆中元素共有O(|V|)個,更新和取出數值的操作有O(|E|)次,因此整個算法的復雜度是O(|E|log(V))
#include <cstdio>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
#define INF (1<<29)
const int maxn = 1010;
typedef pair<int, int> P; //first是最短距離,second是頂點的編號
int V, dist[maxn];
vector<P> G[maxn];
void dijkstra(int s) {
//通過指定greater<P>參數,堆按照first從小到大的順序取出值
priority_queue<P, vector<P>, greater<P> > que;
fill(dist, dist + V , INF);
dist[s] = 0;
while(!que.empty()) que.pop();
que.push(P(0, s));
while(!que.empty()) {
P p = que.top(); que.pop();
int u = p.second;
if(dist[u] < p.first) continue;
int sz = G[u].size();
for(int i=0;i<sz;i++) {
int to = G[u][i].second;
int cost = G[u][i].first;
if(dist[to] > dist[u] + cost) {
dist[to] = dist[u] + cost;
que.push(P(dist[to], to));
}
}
}
}
int main() {
int m;
scanf("%d%d" , &m, &V);
for(int i=0;i<V;i++) G[i].clear();
for(int i=0;i<m;i++) {
int from, to, cost;
scanf("%d%d%d" , &from, &to, &cost);
from --; to --;
G[from].push_back(P(cost, to));
G[to].push_back(P(cost, from));
}
dijkstra(0);
printf("%d\n", dist[V-1]);
return 0;
}
posted @
2015-02-13 19:38 JulyRina 閱讀(319) |
評論 (0) |
編輯 收藏
題目大意:求圖上單點到單點之間的最短路。
題目分析:讓我們考慮沒有負邊的情況。在Bellman-Ford算法中,如果dist[i]還不是最短距離的話,那么即使進行dist[j]=dist[i]+(從i到j的邊的權值)的更新,dist[j]也不會變成最短距離。而且,即使dist[i]沒有變化,每一次循環也要檢查一遍從i出發的所有變。這顯然是很浪費時間的。因此可以對算法作如下修改。
(1)找到最短距離已經確定的頂點,從他出發更新相鄰頂點的最短距離。
(2)此后不再需要關心(1)中的“最短距離已經確定的頂點”。
在(1)和(2)中提到的“最短距離已經確定的”要怎么得到時問題的關鍵。在最開始時,只有起點的最短距離是確定的。而在尚未使用過的頂點中,距離dist[i]最小的頂點就會加入“最短距離已經確定的頂點”的陣營。這是因為由于不會存在負邊,所以dist[i]不會在之后的更新中變小。這個算法叫做Dijkstra算法。
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
#define INF (1<<29)
const int maxn = 1010;
typedef pair<int, int> P;
vector<P> G[maxn];
int V, E, dist[maxn];
bool vis[maxn];
void dijkstra(int s) {
fill(dist, dist + V, INF);
fill(vis, vis + V, false);
dist[s] = 0;
while(true) {
int u = -1;
for(int i=0;i<V;i++)
if(!vis[i] && (u == -1 || dist[i] < dist[u]))
u = i;
if(u == -1) break;
vis[u] = true;
int sz = G[u].size();
for(int i=0;i<sz;i++) {
int v = G[u][i].first;
int w = G[u][i].second;
dist[v] = min(dist[v], dist[u] + w);
}
}
}
int main() {
scanf("%d%d" , &E, &V);
for(int i=0;i<V;i++) G[i].clear();
for(int i=0;i<E;i++) {
int u, v, w;
scanf("%d%d%d" , &u, &v, &w);
u --; v --;
G[u].push_back(make_pair(v, w));
G[v].push_back(make_pair(u, w));
}
dijkstra(0);
printf("%d\n", dist[V-1]);
return 0;
}
posted @
2015-02-13 19:34 JulyRina 閱讀(320) |
評論 (0) |
編輯 收藏
題目大意:求圖上單點到單點之間的最短路。
題目分析:單源最短路問題是固定一個起點,求它到其他所有點的最短路的問題。終點也固定問題叫做兩點之間最短路問題。但是因為單源最短路問題的復雜度是一樣的,因此通常當作單源最短路問題來求解。
記從起點s出發到頂點i的最短距離為dist[i]。則下述等式成立。
dist[i] = min{dist[j]+(從j到i的邊的權值)|e=(j,i)∈E}
如果給定的圖是一個DAG,就可以按托不許給頂點編號,并利用這條遞推關系計算出dist。但是,如果圖中有圈,就無法利用這樣的關系進行計算。
在這種情況下,記當前到頂點i的最短距離為dist[i],并設初值dist[s]=0,dist[i]=INF(足夠大的常數),再不斷使用這條地推關系式更新dist值,就可以算出新的dist。
只要途中不存在負圈,這樣的更新操作就是有限的。結束之后的最短操作就是所求的最短距離了。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
#define INF (1<<29)
const int maxn = 1010, maxm = 4040;
int n, m;
struct Edge { int from, to, cost; } edge[maxm];
int V, E, dist[maxn];
void bellman_ford(int s) {
for(int i=0;i<V;i++) dist[i] = INF;
dist[s] = 0;
while(true) {
bool update = false;
for(int i=0;i<E;i++) {
Edge e = edge[i];
if(dist[e.from] != INF && dist[e.to] > dist[e.from] + e.cost) {
dist[e.to] = dist[e.from] + e.cost;
update = true;
}
}
if(!update) break;
}
}
int main() {
scanf("%d%d" , &m, &n);
V = n; E = 2 * m;
for(int i=0;i<E;i+=2) {
Edge e;
int from, to, cost;
scanf("%d%d%d" , &from, &to, &cost);
from --; to --;
edge[i].from = from;
edge[i].to = to;
edge[i].cost = cost;
edge[i+1].from = to;
edge[i+1].to = from;
edge[i+1].cost = cost;
}
bellman_ford(0);
printf("%d", dist[n-1]);
return 0;
}
這個算法叫做Bellman-Ford算法。如果在圖中不存在從s可達的負圈,那么最短路不會經過同一個頂點兩次(也就是說,最多通過|V|-1次),while(true)的循環最多經過|V|-1次,因此,復雜度是O(VE)。反之,如果存在從s可達的負圈,那么在第|V|次循環中也會更新dist的值,因此也可以用這個性質來檢查負圈。如果一開始對所有的i,都把dist[i]設為0,那么可以檢查出所有的負圈。
bool find_negetive_loop() {
memset(dist, 0, sizeof(dist));
for(int i=0;i<V;i++) {
for(int j=0;j<E;j++) {
if(dist[e.to] > dist[e.from] + e.cost) {
dist[e.to] = dist[e.from] + e.cost;
if(i == V-1) return true;
}
}
}
return false;
}
posted @
2015-02-13 19:32 JulyRina 閱讀(232) |
評論 (0) |
編輯 收藏
題目大意:有N(N<100,000)個人要去M(M<10)個星球,每個人只可以去一些星球,一個星球最多容納Ki個人。請問是否所有人都可以選擇自己的星球
題目分析;直接建立二分圖模型,使用匈牙利算法。
匈牙利算法可以解決多重匹配,原理和二分圖最大匹配很像。注意不要把可以匹配多個的點分割然后按照正常的二分匹配來做,那樣肯定會掛的。
解決多重匹配就是記錄一下多重匹配的點(簡稱Y方點)已經匹配了Pi個點。如果Pi<Ki那么就直接上了,否則的話繼續搜索Yi已經匹配的每一個點并將Yi染色。
因為Yi搜一次就需要染色了,而且Y方點最多是10個,所以每次找增廣路的深度最多是10,這樣就很快了。
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
const int maxn = 100010;
const int maxm = 11;
int y_match[maxn][maxm], g[maxn][maxm], cnt[maxm], capacity[maxn], n, m;
bool vis[maxm];
bool dfs(int x) {
for(int i=0;i<m;i++) {
if(g[x][i] == 0 || vis[i] == true) continue;
vis[i] = true;
if(cnt[i] < capacity[i]) {
y_match[x][cnt[i]++] = x;
return true;
} else {
for(int j=0;j<capacity[i];j++) {
if(dfs(y_match[i][j]) == true) {
y_match[i][j] = x;
return true;
}
}
}
}
return false;
}
bool hungary(int n) {
for(int i=0;i<n;i++) {
memset(vis, false, sizeof(bool)*(m));
if(dfs(i) == false)
return false;
}
return true;
}
int main() {
while(~scanf("%d%d" , &n, &m)) {
memset(cnt, 0, sizeof(int)*(n));
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
scanf("%d", &g[i][j]);
for(int i=0;i<m;i++)
scanf("%d", &capacity[i]);
if(hungary(n) == true)
puts("YES");
else
puts("NO");
}
return 0;
}
posted @
2015-02-13 16:23 JulyRina 閱讀(1421) |
評論 (0) |
編輯 收藏
題目大意:動物王國中有三類動物A,B,C,這三類動物的食物鏈構成了有趣的環形。A吃B, B吃C,C吃A。
現有N個動物,以1-N編號。每個動物都是A,B,C中的一種,但是我們并不知道它到底是哪一種。
有人用兩種說法對這N個動物所構成的食物鏈關系進行描述:
第一種說法是"1 X Y",表示X和Y是同類。
第二種說法是"2 X Y",表示X吃Y。
此人對N個動物,用上述兩種說法,一句接一句地說出K句話,這K句話有的是真的,有的是假的。當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。
1) 當前的話與前面的某些真的話沖突,就是假話;
2) 當前的話中X或Y比N大,就是假話;
3) 當前的話表示X吃X,就是假話。
你的任務是根據給定的N(1 <= N <= 50,000)和K句話(0 <= K <= 100,000),輸出假話的總數。
題目分析;由于N和K很大,所以必須高效的維護動物之間的關系,并快速判斷是否殘生了矛盾,所以決定采用并查集。
對于每只動物i創建3個元素i,i+N,i+2N,并用這3*N個元素創建并查集。這個并查集維護如下信息:
i+xN表示“i屬于種類x”。
并查集里的每一個組表示組內所有元素代表的都同時發生或不發生。
同時,在合并之前先判斷合并是否會產生矛盾。
#include <cstdio>
#include <iostream>
using namespace std;
const int maxn = 50050;
int father[maxn*3], n, m, ans;
void init() {
for(int i=0;i<3*n;i++) father[i] = i;
}
int find(int x) {
return x == father[x] ? x : father[x] = find(father[x]);
}
void unite(int x, int y) {
int a = find(x) , b = find(y);
father[a] = father[b] = father[x] = father[y] = min(a, b);
}
bool check(int d, int x, int y) {
if(x >= n || y >= n || x < 0 || y < 0)
return false;
if(d == 2 && x == y)
return false;
if(d == 1) {
if(find(x) == find(y+n) || find(x) == find(y+2*n))
return false;
unite(x, y);
unite(x+n, y+n);
unite(x+2*n, y+2*n);
return true;
} else {
if(find(x) == find(y) || find(x) == find(y+2*n))
return false;
unite(x, y+n);
unite(x+n, y+2*n);
unite(x+2*n, y);
return true;
}
}
int main() {
scanf("%d%d" , &n, &m);
init();
ans = 0;
for(int i=0;i<m;i++) {
int d, x, y;
scanf("%d%d%d", &d, &x, &y);
x --; y --;
if(check(d, x, y) == false)
ans ++;
}
printf("%d\n", ans);
return 0;
}
posted @
2015-02-11 17:33 JulyRina 閱讀(287) |
評論 (0) |
編輯 收藏
題目大意:給定長度為N的字符串S,要構造一個長度為N的字符串T。起初,T是一個空串,隨后反復進行下列任意操作。
從S的頭部刪除一個字符,加到T的尾部
從S的尾部刪除一個字符,加到T的尾部
目標是構造字典序盡可能小的字符串。
題目分析;從字典序的性質上看,無論T的末尾有多大,只要前面部分較小就可以。所以我們可以試一下如下貪心算法:
不斷取S和T的末尾中較小的一個字符放到T的末尾
這個算法已經接近正確了,只是針對S的開頭和末尾字符相同的情形還沒有定義。在這種情況下,因為我們希望能夠盡早使用更小的字符,所以就要比較一下下一個字符的大小。下一個字符也有可能相同,因此就有如下算法:
按照字典序比較字符串S和S反轉后的字符串S'。
如果S較小,就從S的開頭取出一個文字,放到T的末尾。
如果S'較小,就從S的末尾取出一個文字,放到T的末尾。
(若果相同則去哪一個都可以)
#include <cstdio>
char s[2002], tmp[2];
int n;
int main() {
while(~scanf("%d", &n)) {
for(int i=0;i<n;i++) {
scanf("%s", tmp);
s[i] = tmp[0];
}
int l = 0, r = n - 1, cnt = 0;
while(l <= r) {
int i = 0, left = true;
while(l + i <= r - i) {
if(s[l+i] < s[r-i]) {
break;
}
if(s[l+i] > s[r-i]) {
left = false;
break;
}
i ++;
}
if(left == true) putchar(s[l++]);
else putchar(s[r--]);
cnt ++;
if(cnt % 80 == 0) putchar('\n');
}
if(cnt % 80) putchar('\n');
}
return 0;
}
posted @
2015-02-11 15:59 JulyRina 閱讀(2225) |
評論 (0) |
編輯 收藏
題目大意:有一個大小為N*M的園子,雨后積起了水。八連通的積水被認為是連接在一起的。請求出園子里總共有多少水洼?
(八連通指下圖中相當于W的*)
***
*W*
***
題目分析;從任意的W開始,不同的用一個vis數組標記是否訪問過。一次DFS后與初始的這個W相連的W就都被標記過了。因此知道途中不再存在W為止,總共進行DFS的次數就是答案了。
#include <cstdio>
#include <cstring>
const int maxn = 101;
int n, m, ans;
char g[maxn][maxn];
bool vis[maxn][maxn];
void dfs(int x, int y) {
if(vis[x][y] == true || g[x][y] != 'W') return;
vis[x][y] = true;
for(int i=-1;i<=1;i++)
for(int j=-1;j<=1;j++) {
if(!i && !j || x + i < 0 || x + i >= n || y + j < 0 || y + j >= m) continue;
dfs(x+i, y+j);
}
return;
}
int main() {
while(~scanf("%d%d" , &n, &m)) {
for(int i=0;i<n;i++) {
scanf("%s", g[i]);
memset(vis+i, false, sizeof(bool)*(m));
}
ans = 0;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if(g[i][j] == 'W' && !vis[i][j]) {
ans ++;
dfs(i, j);
}
printf("%d\n", ans);
}
return 0;
}
posted @
2015-02-11 15:29 JulyRina 閱讀(536) |
評論 (0) |
編輯 收藏