貪心算法
貪心算法總是作出在當前看來最好的選擇。也就是說貪心算法并不從整體最優考慮,它所作出的選擇只是在某種意義上的局部最優選擇。當然,希望貪心算法得到的最終結果也是整體最優的。雖然貪心算法不能對所有問題都得到整體最優解,但對許多問題它能產生整體最優解,如之前的Dijkstra算法,Prim算法,Kruskal算法。如果不要求絕對最佳答案,那么有時候我們使用簡單的貪婪算法生成近似的答案.
-
貪心與動態規劃
貪心算法和動態規劃都需求最優子結構,但是貪心算法是自頂向下方式進行,就是每一步,根據策略得到一個當前最優解。傳遞到下一步,從而保證每一步都是選擇當前最優的。最后得到結果.每一步的最優解都依賴上一部的最優解.你只考慮之前已做出的選擇
而動態規劃通常自底向上解各種子問題,每一步,根據策略得到一個更小規模的問題。最后解決最小規模的問題。得到整個問題最優解.全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解.你考慮的都是以后的子問題
經典的還是背包問題,之前的01背包問題我們采用動態規劃解決而不能用貪心.但是如果改成部分背包問題呢:
假如有三件物品,背包可裝50磅的物品,物品1重10磅,價值60元;物品2重20磅,價值100元;物品3重30磅,價值120元。你可以選擇帶走每個物品的全部或一部分,求如何選擇可以使背包所裝的價值最大?
注意到不同點是我們可以選擇帶走一部分,所以使用貪心算法十分自然地想到,先算含金量啊,先把含金量最高的都帶完,再帶含金量其次的...很容易得到解,帶走一件1,一件2,2/3件3...比較簡單代碼不寫.
-
活動安排問題
設有n個活動的集合E = {1,2,…,n},其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。每個活i都有一個要求使用該資源的起始時間si和一個結束時間fi,且si < fi 。如果選擇了活動i,則它在半開時間區間[si, fi)內占用資源。若區間[si, fi)與區間[sj, fj)不相交,則稱活動i與活動j是相容的。也就是說,當si >= fj或sj >= fi時,活動i與活動j相容.怎么盡可能地安排多的相容活動呢?
設待安排的11個活動的開始時間和結束時間按結束時間的非減序排列如下:
注意要按結束時間的早晚排列,沒排好的話,你可以回去用各種方法自己排.既然貪心么就是越早結束越好,給后面留盡可能多的空間.其次"目光短淺",從排列好的里一個個選,能選一個是一個,別管后面的...
顯然,我們選擇到了
(1)1-4,(4)5-7,(8)8-11,(11)12-14
感覺不靠譜么,其實對于這個活動安排問題,貪心算法總能求得的整體最優解,即它最終所確定的相容活動集合A的規模最大。這個結論可以用數學歸納法證明。
我們還是來代碼:
package com.fredal.structure;
import java.util.Arrays;
public class Arrange {
public static int[] greedyArrangement(int[] start,int[] end){
int total=start.length;
int endtime=end[0];//選擇的所有活動的最末結束時間
int[] arrangement=new int[total];
arrangement[0]=1;//無腦選第一個 最早結束的那個
int count=1;
for(int i=0;i<total;i++){
if(start[i]>endtime){//下一個活動開始時間晚于當前活動結束時間
arrangement[count++]=i+1;//活動選中
endtime=end[i];//更新結束時間
}
}
return arrangement;
}
public static void main(String[] args) {
int[] start={1,3,0,5,3,5,6,8,8,2,12};
int[] end={4,5,6,7,8,9,10,11,12,13,14};
int[] arrangement=greedyArrangement(start, end);
for(int i=0;i<arrangement.length;i++){
if(arrangement[i]!=0)
System.out.println("開始時間:"+start[arrangement[i]-1]+",結束時間:"+end[arrangement[i]-1]);
}
}
}
-
哈夫曼編碼
哈夫曼編碼是廣泛地用于數據文件壓縮的十分有效的編碼方法。其壓縮率通常在20%~90%之間。哈夫曼編碼算法用字符在文件中出現的頻率表來建立一個用0,1串表示各字符的最優表示方式。一個包含100,000個字符的文件,各字符出現頻率不同,如下表所示
我們可以求得對于標準編碼位數需要
(45+13+12+16+9+5)*3=300
,而對于變長碼45×1+13×3+12×3+16×3+9×4+5×4=224
,壓縮了很多...首先要講一講前綴碼:對每一個字符規定一個0,1串作為其代碼,并要求任一字符的代碼都不是其他字符代碼的前綴。這種編碼稱為前綴碼。
我們可以用二叉樹作為前綴碼的數據結構:樹葉表示給定字符;從樹根到樹葉的路徑當作該字符的前綴碼;代碼中每一位的0或1分別作為指示某節點到左兒子或右兒子的“路標”.字符只放在樹葉上,滿二叉樹是其基本特征,你知道放法太多了,所以關鍵問題變成了怎么尋找總價值最小的完全二叉樹,即最優前綴碼.
對于該例樣本字母表的最優樹如下b,位數正是224,圖a不是完全二叉樹顯然不符合:
那么怎么尋找的呢,就是哈夫曼編碼干的事了.構造過程如下:
假設編碼字符集中每一字符c的頻率是f(c)。以f為鍵值的優先隊列Q用在貪心選擇時有效地確定算法當前要合并的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合并后,產生一棵新的樹,其頻率為合并的2棵樹的頻率之和,并將新樹按順序插入優先隊列Q。經過n-1次的合并后,優先隊列中只剩下一棵樹,即所要求的樹T
代碼實現,這里需要使用完全二叉樹,發現之前寫的沒有特別符號要求的,就這里直接實現吧.還有優先隊列類,用之前實現過的MyHeap,有需要可以查看堆的實現.
package com.fredal.structure;
public class Huffman {
static MyHeap<Node> heap=new MyHeap<Node>();//堆類
static class Node implements Comparable<Node>{
private int weight;//權值 頻率
private String value;//字符
private Node left;
private Node right;
private Node parent;
private String path;//記錄路徑
private boolean isvisited;//是否遍歷過
public Node(int weight, String value) {
super();
this.weight = weight;
this.value = value;
}
public int compareTo(Node o) {
return weight-o.weight;
}
}
public static Node bulidHuffman(Node[] nodes){
for(int i=0;i<nodes.length;i++){
heap.insert(nodes[i]);
}
while(heap.getCurrentSize()>1){
Node minA = heap.deleteMin();//彈出最小的兩個
Node minB = heap.deleteMin();
Node sumNode=new Node(minA.weight+minB.weight,minA.value+minB.value);//權值和稱為其父節點
//維護關系
sumNode.left=minA;
minA.path="0";//為了方便 直接把路徑信息記這兒了
sumNode.right=minB;
minB.path="1";
minA.parent=sumNode;
minB.parent=sumNode;
heap.insert(sumNode);//插入堆
}
return heap.findMin();//返回最后一個 相當于是完整的樹了
}
public static void printHuffman(Node node){
if(node.left!=null && !node.left.isvisited){//遍歷左邊
Node left = node.left;
left.isvisited=true;
printHuffman(left);
}
if(node.right!=null && !node.right.isvisited){//遍歷右邊
Node right=node.right;
right.isvisited=true;
printHuffman(right);
}
if(node.left==null && node.right==null){//是葉子節點
StringBuffer sb=new StringBuffer();
sb.append(node.path);
System.out.print(node.value+":");
while(node.parent!=null){
node=node.parent;
if(node.path!=null)
sb.append(node.path);//訪問父節點 獲得路徑信息
}
System.out.println(sb.reverse().toString());//輸出
printHuffman(node);//遞歸 輸出下一個葉子節點的編碼
}
}
public static void main(String[] args) {
Node[] nodes={
new Node(45, "a"),
new Node(13, "b"),
new Node(12, "c"),
new Node(16, "d"),
new Node(9, "e"),
new Node(5, "f")
};
Node root = bulidHuffman(nodes);
printHuffman(root);
}
}
-
近似裝箱問題
給定N 項物品,大小為 s1, s2, ..., sN,所有的大小都滿足 0 < si < = 1 ;問題是要把這些物品裝到最小數目的箱子中去, 已知每個箱子的容量是1個單位;下圖顯示的是對N項物品的最優裝箱方法
這個問題有兩種版本.第一種是聯機裝箱問題,必須將每一件物品放入一個箱子后才處理下一件物品.另外一種是脫機裝箱問題,我們做任何事情都需要等到所有的輸入數據被讀取后才進行.
我們先來考慮聯機裝箱的三種算法,第一種是下項適合算法: 當處理任一物品時,我們檢查看他是否還能裝進剛剛裝進物品的同一個箱子中去.如果能夠裝進去,那么就把它裝入該箱子,否則,就開辟一個新箱子.例子如下:
我們用代碼模擬:
package com.fredal.structure;
import java.util.LinkedList;
public class BinPacking {
static LinkedList<Box> boxes=new LinkedList<Box>();//存儲所有箱子
static int index=1;
static class Box{//箱子類
private double remain;//剩余容量
private LinkedList<Double> values;
public Box(){
remain=1;//設容量初始為1
values=new LinkedList<Double>();//存儲箱子中的物品
}
}
//下項適合算法
public static void nextfit(double[] a){
for(int i=0;i<a.length;i++){
if(boxes.peek()==null)
boxes.push(new Box());
Box last = boxes.peek();
if(last.remain>=a[i]){//裝的下就裝
last.values.add(a[i]);
last.remain-=a[i];
}else{//裝不下就開辟新箱子
Box nbox=new Box();
nbox.values.add(a[i]);
nbox.remain-=a[i];
boxes.push(nbox);
}
}
show(boxes);
}
//輸出顯示
public static void show(LinkedList<Box> boxes){
while(!boxes.isEmpty()){
Box box = boxes.removeLast();
System.out.print("box"+index+++":");
while(!box.values.isEmpty()){
Double value = box.values.removeFirst();
System.out.print(value+" ");
}
System.out.println();
}
}
public static void main(String[] args) {
double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
nextfit(a);
}
}
下項算法的性能是線性的,但是在實踐中顯然是不靠譜的.不需要開辟新箱子的時候開辟了新箱子.接下來講首次適合算法:依序掃描這些箱子把新的一項物品放入足夠盛下它的第一個箱子中.
代碼如下,注意show()函數有點變化的:
package com.fredal.structure;
import java.util.Iterator;
import java.util.LinkedList;
public class BinPacking {
static LinkedList<Box> boxes=new LinkedList<Box>();//存儲所有箱子
static int index=1;
static class Box{//箱子類
private double remain;//剩余容量
private LinkedList<Double> values;
public Box(){
remain=1;//設容量初始為1
values=new LinkedList<Double>();//存儲箱子中的物品
}
}
//首次適合算法
public static void firstfit(double[] a){
for(int i=0;i<a.length;i++){
boolean flag=false;
if(boxes.peek()==null)
boxes.add(new Box());
Iterator<Box> it = boxes.iterator();
while(it.hasNext()){//從頭到尾遍歷 能裝就裝
Box box = it.next();
if(box.remain>=a[i]){
box.values.add(a[i]);
box.remain-=a[i];
flag=true;
break;
}
}
if(!flag){//全部不能裝就開辟新的
Box nbox=new Box();
nbox.values.add(a[i]);
nbox.remain-=a[i];
boxes.add(nbox);
}
}
show(boxes);
}
//輸出顯示
public static void show(LinkedList<Box> boxes){
while(!boxes.isEmpty()){
Box box = boxes.removeFirst();
System.out.print("box"+index+++":");
while(!box.values.isEmpty()){
Double value = box.values.removeFirst();
System.out.print(value+" ");
}
System.out.println();
}
}
public static void main(String[] args) {
double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
firstfit(a);
}
}
第三個是最佳適合算法,該方法不是吧一項新物品放入所發現的第一個能夠容納它的箱子,而是放到所有箱子中能夠容納它的最滿的箱子中.該算法對隨記的輸入表現的更好
代碼如下,注意輸出函數還是變了,并且采用了ArrayList存儲:
package com.fredal.structure;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class BinPacking {
static ArrayList<Box> boxes=new ArrayList<Box>();//存儲所有箱子
static int index=1;
static class Box{//箱子類
private double remain;//剩余容量
private LinkedList<Double> values;
public Box(){
remain=1;//設容量初始為1
values=new LinkedList<Double>();//存儲箱子中的物品
}
}
//最佳適合算法
public static void bestfit(double[] a){
for(int i=0;i<a.length;i++){
double remainMin=1+1;//最小的剩余容量 用于尋找最滿的箱子 初始化表示比容量大1
int index=0;//記錄箱子編號
if(boxes.size()==0)
boxes.add(new Box());
for(int j=0;j<boxes.size();j++){
Box box=boxes.get(j);
if(box.remain>=a[i] && box.remain<remainMin){//從頭遍歷 如果找到更滿的并且能裝下的就記錄
remainMin=box.remain;
index=j;
}
}
if(remainMin<=1){//說明找到了可以裝的
Box box = boxes.get(index);//裝進記錄好的最滿的箱子
box.values.add(a[i]);
box.remain-=a[i];
}else{//找不到可以裝的就開辟新的箱子
Box nbox=new Box();
nbox.values.add(a[i]);
nbox.remain-=a[i];
boxes.add(nbox);
}
}
show(boxes);
}
//輸出顯示
public static void show(ArrayList<Box> boxes){
for(int i=0;i<boxes.size();i++){
Box box=boxes.get(i);
System.out.print("box"+index+++":");
while(!box.values.isEmpty()){
Double value = box.values.removeFirst();
System.out.print(value+" ");
}
System.out.println();
}
}
public static void main(String[] args) {
double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
bestfit(a);
}
}
接下來是脫機算法,顯然脫機算法可以表現得更好.聯機算法的問題在于在于將大項物品裝箱困難,特別是當他們在輸入的晚期出現的時候.于是脫機算法我們可以將各項物品排序,把最大的物品放在最先,此時我們可以應用首次適合算法或最佳適合算法,分別得到“首次適合遞減算法” 和 ”最佳適合遞減算法”.
這兩種算法是差不多的,我們以首次適合遞減算法為例.
代碼如下,我們使用快速排序.注意java中double數有些精度問題,上面的三個算法也會出現可能無法完全裝滿的問題,我就不去改了.這里改一下
package com.fredal.structure;
import java.util.Iterator;
import java.util.LinkedList;
public class BinPacking {
static LinkedList<Box> boxes=new LinkedList<Box>();//存儲所有箱子
static int index=1;
static class Box{//箱子類
private double remain;//剩余容量
private LinkedList<Double> values;
public Box(){
remain=1;//設容量初始為1
values=new LinkedList<Double>();//存儲箱子中的物品
}
}
//首次適合遞減算法
public static void firstfit(double[] a){
quickSort(a, 0, a.length-1);
for(int i=0;i<a.length;i++){
boolean flag=false;
if(boxes.peek()==null)
boxes.add(new Box());
Iterator<Box> it = boxes.iterator();
while(it.hasNext()){//從頭到尾遍歷 能裝就裝
Box box = it.next();
if(box.remain>a[i]||Math.abs(box.remain-a[i])<Math.pow(10, -10)){//解決一下精度
box.values.add(a[i]);
box.remain-=a[i];
flag=true;
break;
}
}
if(!flag){//全部不能裝就開辟新的
Box nbox=new Box();
nbox.values.add(a[i]);
nbox.remain-=a[i];
boxes.add(nbox);
}
}
show(boxes);
}
//快速排序
public static void quickSort(double[] a,int left,int right){
if(left<right){//遞歸出口條件
int i=left;//左指針
int j=right;//右指針
double x=a[left];//選擇第一個元素作為標尺
while(i<j){
while(i<j && a[j]<=x) j--;//從右向左找第一個大于x的數
if(i<j) a[i++]=a[j];
while(i<j && a[i]>x) i++;//從左向右找第一個小于等于x的數
if(i<j) a[j--]=a[i];
}
a[i]=x;//插入標尺
quickSort(a,left,i-1);//遞歸左邊
quickSort(a, i+1, right);//遞歸右邊
}
}
//輸出顯示
public static void show(LinkedList<Box> boxes){
while(!boxes.isEmpty()){
Box box = boxes.removeFirst();
System.out.print("box"+index+++":");
while(!box.values.isEmpty()){
Double value = box.values.removeFirst();
System.out.print(value+" ");
}
System.out.println();
}
}
public static void main(String[] args) {
double[] a={0.2,0.5,0.4,0.7,0.1,0.3,0.8};
firstfit(a);
}
}
分治算法
分治法是一種很重要的算法。字面上的解釋是“分而治之”,就是把一個復雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合并。這個技巧是很多高效算法的基礎.
分治策略是:對于一個規模為n的問題,若該問題可以容易地解決(比如說規模n較小)則直接解決,否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解這些子問題,然后將各子問題的解合并得到原問題的解。這種算法設計策略叫做分治法.
傳統上,含有兩個或以上的遞歸調用的叫做分治法.經典的例子就是快速排序,歸并排序
接下來我們選一些其他的經典例子來分析
-
大整數乘法
設有兩個大整數相乘,X=61438521,Y=94736407.那么XY=5820464730934047.易知我么的算法需要O(N2)即O(82)次操作.
如果我們把X和Y都拆成兩半,由最高幾位和最低幾位組成.那么XL=6143,XR=8521,YL=9473,YR=6470.于是X=XL*10^4+XR
,Y=YL*10^4+YR
.可以得到
XY=XL*YL*10^8+(XL*YR+XR*YL)*10^4+XRYR
顯然這個式子就是由4個乘法組成的,每一個都是原問題的一半,而108,104的乘法只是添一些0,于是可以得到遞歸:T(N)=4T(N/2)+O(N).
.
我們按照主定理(相關資料查閱維基百科),可以求得算法復雜度仍然是O(N2).并沒有改進這個問題.
觀察XL*YR+XR*YL
,可以分解為(XL-XR)(YR-YL)+XL*YL+XR*YR
,我們僅需要算前面一項,后面的兩項已經計算過了.于是得到了T(N)=3T(N/2)+O(N).
,按照主定理,可得T(N)=O(N^1.59).當然對于每一個乘積我們還可以繼續遞歸下去,一般到四位數就不用遞歸了.
我們還是采取代碼來模擬過程:
package com.fredal.structure;
public class BigMultiply {
//大整數相乘
public static String multiply(String x,String y){
int flag1=0;//x的符號位
int flag2=0;//y的符號位
if(x.charAt(0)=='-'){//處理符號
x=x.substring(1);//先把符號位截掉
flag1=1;
}
if(y.charAt(0)=='-'){
y=y.substring(1);
flag2=1;
}
String flag=(flag1^flag2)==1?"-":"";//相乘即異或之后符號位
if(x.length()<y.length())//保證x的位數更大
return flag+multiply(y, x);
if(x.length()<=4)
return flag+Integer.parseInt(x)*Integer.parseInt(y);//少于等于四位數直接計算了
if(x.length()%2==0){//x位數是偶數 就把y補成和x一樣長
while(x.length()>y.length())
y="0"+y;
}else{//x位數不是偶數 就先把x補成偶數 再把y補成和x一樣長
x="0"+x;
while(x.length()>y.length())
y="0"+y;
}
String xl=x.substring(0,x.length()/2);
String xr=x.substring(x.length()/2);
String yl=y.substring(0,y.length()/2);
String yr=y.substring(y.length()/2);
String D1=minus(xl, xr);//xl-xr
String D2=minus(yr, yl);//yr-yl
String xlyl=multiply(xl, yl);//xl*yl
String xryr=multiply(xr, yr);//xr*yr
String D3=add(multiply(D1, D2)+"", add(xlyl, xryr));//D1*D2+Xl*Yl+Xr*Yr
return flag+add(shift(xlyl, x.length()),add(shift(D3, x.length()/2),xryr));//Xl*Yl*10^n+D3*10^(n/2)+Xr*Yr
}
//大數相減 帶符號處理
public static String minus(String x,String y){
int large=compare(x, y);
String flag=large>=0?"":"-";//加上符號
if (large==0)
return "0";
else if(large>0)//轉化成大的減小的
return minusBigNum(x,y);
else
return flag+minusBigNum(y,x);
}
//大數相減
private static String minusBigNum(String x,String y){//大數減小數
int len=x.length();
while(len>y.length())
y="0"+y;
StringBuilder result=new StringBuilder();
int flag=0;//表示是否進位
for(int i=len-1;i>=0;i--){
int xs=Integer.parseInt(String.valueOf(x.charAt(i)));
int ys=Integer.parseInt(String.valueOf(y.charAt(i)));
if(xs+flag>=ys){//別忘了把flag加上
result.append(xs-ys+flag);
flag=0;
}else{
result.append(10+xs-ys+flag);
flag=-1;
}
}
return clearZero(result.reverse().toString());
}
//大數相加
public static String add(String x,String y){
if(x.charAt(0)=='-'){//先處理符號
x=x.substring(1);
if(y.charAt(0)=='-'){
y=y.substring(1);
return "-"+add(x,y);
}else
return minus(y, x);
}
if(y.charAt(0)=='-'){
y=y.substring(1);
return minus(x, y);
}
if(x.length()<y.length())
return add(y, x);//保證x的位數更大
int len=x.length();
while(len>y.length())//補位使位數相等
y="0"+y;
StringBuilder result=new StringBuilder();
int flag=0;//表示是否進位
for(int i=len-1;i>=0;i--){
int xs=Integer.parseInt(String.valueOf(x.charAt(i)));
int ys=Integer.parseInt(String.valueOf(y.charAt(i)));
if(xs+ys+flag>9){//別忘了把flag加上
result.append(xs+ys-10+flag);
flag=1;
}else{
result.append(xs+ys+flag);
flag=0;
}
}
if(flag!=0)
result.append(1);
return clearZero(result.reverse().toString());
}
//計算10n次方的 就是后面加0
public static String shift(String x,int n){
for(int i=0;i<n;i++){
x+="0";
}
return x;
}
//消除0
private static String clearZero(String str){
int i=0;
while(i<str.length()&&str.charAt(i)=='0'){
i++;
}
return str.substring(i);
}
//比較兩個數的大小
private static int compare(String x,String y){
if(x.length()>y.length())
return 1;
else if(x.length()<y.length())
return -1;
else{
int index = 0;
while (index < x.length() && x.charAt(index) == y.charAt(index))
index++;
if (index == x.length())
return 0;
else {
return x.charAt(index) > y.charAt(index)? 1 : -1;
}
}
}
public static void main(String[] args) {
System.out.println(multiply("-61438521", "94736407"));
System.out.println(multiply("-3124234254543411432432422238221342421",
"-2423442342342342342342342323423445345699"));
}
}
算法并不復雜,但是處理符號什么的還是比較麻煩的,而且加法減法啥的都要自己去實現.
-
Strassen矩陣乘法
兩個矩陣的乘法學過線性代數的都知道怎么求,一般來說復雜度為O(N^3).直接給出標準的算法
package com.fredal.structure;
public class MartixMultiply {
public static int[][] multiply(int[][] a, int[][] b) {
int n = a.length;
int[][] c = new int[n][n];
for (int i = 0; i < n; i++)
// 初始化
for (int j = 0; j < n; j++)
c[i][j] = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
c[i][j] += a[i][k] * b[k][j];
return c;
}
public static void main(String[] args) {
int[][] a = { { 1, 2 }, { 3, 4 } };
int[][] b = { { 3, 4 }, { 7, 2 } };
int[][] c = multiply(a, b);
System.out.println(c[0][0] + " " + c[0][1] + " " + c[1][0] + " "
+ c[1][1]);
}
}
Strassen提出了算法打破了O(N^3)的屏障.用到分治算法,把矩陣分為4塊.
其中:
可以得到遞推關系
T(N)=7T(N/2)+O(N2)
,依據主定理得到解T(N)=O(N^2.81).這兒不做出證明,顯然這用到了分治法的思想,我們用代碼模擬
package com.fredal.structure;
public class MartixMultiply {
public static int[][] StrassenMultiply(int[][] a, int[][] b) {
int[][] result = new int[a.length][b.length];
if (a.length == 2)
return multiply(a, b);// 如果是2階的 就結束遞歸 用傳統方法
// a的四個子矩陣
int[][] A00 = divide(a, 1);
int[][] A01 = divide(a, 2);
int[][] A10 = divide(a, 3);
int[][] A11 = divide(a, 4);
// b的四個子矩陣
int[][] B00 = divide(b, 1);
int[][] B01 = divide(b, 2);
int[][] B10 = divide(b, 3);
int[][] B11 = divide(b, 4);
int[][] m1 = StrassenMultiply(addArrays(A00, A11), addArrays(B00, B11));
int[][] m2 = StrassenMultiply(addArrays(A10, A11), B00);
int[][] m3 = StrassenMultiply(A00, subArrays(B01, B11));
int[][] m4 = StrassenMultiply(A11, subArrays(B10, B00));
int[][] m5 = StrassenMultiply(addArrays(A00, A01), B11);
int[][] m6 = StrassenMultiply(subArrays(A10, A00), addArrays(B00, B01));
int[][] m7 = StrassenMultiply(subArrays(A01, A11), addArrays(B10, B11));
int[][] C00 = addArrays(m7, subArrays(addArrays(m1, m4), m5));// m1+m4-m5+m7
int[][] C01 = addArrays(m3, m5); // m3+m5
int[][] C10 = addArrays(m2, m4); // m2+m4
int[][] C11 = addArrays(m6, subArrays(addArrays(m1, m3), m2));// m1+m3-m2+m6
// 將四個矩陣合并起來
Merge(result, C00, 1);
Merge(result, C01, 2);
Merge(result, C10, 3);
Merge(result, C11, 4);
return result;
}
// /分割得到子矩陣
private static int[][] divide(int[][] a, int flag) {
int[][] result = new int[a.length / 2][a.length / 2];
switch (flag) {
case 1:
for (int i = 0; i < a.length / 2; i++)
for (int j = 0; j < a.length / 2; j++)
result[i][j] = a[i][j];
break;
case 2:
for (int i = 0; i < a.length / 2; i++)
for (int j = a.length / 2; j < a.length; j++)
result[i][j - a.length / 2] = a[i][j];
break;
case 3:
for (int i = a.length / 2; i < a.length; i++)
for (int j = 0; j < a.length / 2; j++)
result[i - a.length / 2][j] = a[i][j];
break;
case 4:
for (int i = a.length / 2; i < a.length; i++)
for (int j = a.length / 2; j < a.length; j++)
result[i - a.length / 2][j - a.length / 2] = a[i][j];
break;
}
return result;
}
// 矩陣加法
private static int[][] addArrays(int[][] a, int[][] b) {
int[][] result = new int[a.length][a.length];
for (int i = 0; i < result.length; i++) {
for (int j = 0; j < result.length; j++) {
result[i][j] = a[i][j] + b[i][j];
}
}
return result;
}
// 矩陣減法
private static int[][] subArrays(int[][] a, int[][] b) {
int[][] result = new int[a.length][a.length];
for (int i = 0; i < result.length; i++) {
for (int j = 0; j < result.length; j++) {
result[i][j] = a[i][j] - b[i][j];
}
}
return result;
}
// 將b復制到a的指定位置
private static void Merge(int[][] a, int[][] b, int flag) {
switch (flag) {
case 1:
for (int i = 0; i < a.length / 2; i++)
for (int j = 0; j < a.length / 2; j++)
a[i][j] = b[i][j];
break;
case 2:
for (int i = 0; i < a.length / 2; i++)
for (int j = a.length / 2; j < a.length; j++)
a[i][j] = b[i][j - a.length / 2];
break;
case 3:
for (int i = a.length / 2; i < a.length; i++)
for (int j = 0; j < a.length / 2; j++)
a[i][j] = b[i - a.length / 2][j];
break;
case 4:
for (int i = a.length / 2; i < a.length; i++)
for (int j = a.length / 2; j < a.length; j++)
a[i][j] = b[i - a.length / 2][j - a.length / 2];
break;
}
}
// 常規做法
public static int[][] multiply(int[][] a, int[][] b) {
int n = a.length;
int[][] c = new int[n][n];
for (int i = 0; i < n; i++)
// Initialization
for (int j = 0; j < n; j++)
c[i][j] = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
c[i][j] += a[i][k] * b[k][j];
return c;
}
public static void main(String[] args) {
int[][] a = { { 1, 2, 6, 7 }, { 3, 4, 5, 4 }, { 5, 8, 3, 8 },
{ -6, 4, 3, 9 } };
int[][] b = { { 3, 4, 9, 0 }, { 7, 2, -5, -6 }, { 0, 7, -4, 6 },
{ -6, 3, -5, 4 } };
int[][] c = multiply(a, b);
System.out.println(c[0][0] + " " + c[0][1] + " " + c[1][0] + " "
+ c[1][1]);
}
}
-
最近點對問題
這個問題真的很有意思,給定空間上的n個節點S={(xi,yi)},如何查找這n個點對中最近的點對的距離?
我們都知道兩點間距離:((xi-xj)2+(yi-yj)2)1/2
那么如果使用暴力搜索,需要兩兩檢測,需要花費O(N2).
我們可以采用分治法的思想.首先我們假設這些點都已經按照x坐標排序過,那么可以在中間畫一條線,把點集分為Pl和Pr,那么最近的一對點要么都在Pl中,要么都在Pr中,要么分別在Pl和pr中,把這三個距離分別用dl,dr,dc表示.
顯然我們可以遞歸第計算dl和dr,關鍵是計算dc.令δ=min(dl,dr),顯然我們可以做出雙道帶,來縮小考慮的范圍
對于在帶中的,我們可以通過兩層循環蠻力計算,但畢竟還是不好,最壞的情況是說有點都可能在這帶狀區域里.那么考慮對y坐標排序.如果pi和pj的y坐標差大于δ,那么可以直接break跳出內循環.如對于p3來說,我們只需要考慮p4和p5,之后的break就行了.
還有一點可優化的是,其實對于任意的點pi,最多有7個點需要被考慮.
如圖所示,這是點最多的情況.因為如果在上圖的情況中隨便再添一個點,比如在左邊,那么這個點距離左邊其他四個點的距離肯定有小于δ的(最均勻矩形中心,距離小于δ).那么只要距離一個點小于δ.那么與之前的假設在Pl中最短的距離為δ矛盾了!所以我們只需考慮最壞情況,某個角上是pi,那么其余還有7個點要考慮,得證.
接下來就容易了,但是還有要注意的是,不能每次遞歸都去排序x,y坐標.可以保留兩個表,一個x坐標排序的表,一個y坐標排序的表.這個算法是O(NlogN)的.
還是來實現吧:
package com.fredal.structure;
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
public class Distance {
static final int NUM=10;//使用窮舉法的點數
public static void main(String[] args) {
Set<Point> testData = new TreeSet<Point>();
java.util.Random random = new java.util.Random();
for(int i = 0;i < 100000;i++){
int x = random.nextInt(100000);
int y = random.nextInt(100000);
testData.add(new Point(x, y));
}
Point [] points = new Point[testData.size()];
points = (Point[]) testData.toArray(points);
Point[] result=new Point[2];
long startTime=System.currentTimeMillis(); //獲取開始時間
result=findpair(points);
long endTime=System.currentTimeMillis(); //獲取結束時間
System.out.println("最近點: ("+result[0].getX()+","+result[0].getY()
+")--("+result[1].getX()+","+result[1].getY()+")");
System.out.println("距離為: "+distance(result[0], result[1]));
System.out.println("分治法運行時間:"+(endTime-startTime)+"ms");
long startTime2=System.currentTimeMillis(); //獲取開始時間
result=bong(points);
long endTime2=System.currentTimeMillis(); //獲取結束時間
System.out.println("窮舉法運行時間:"+(endTime2-startTime2)+"ms");
}
//尋找最近點對
public static Point[] findpair(Point[] p){
Point[] result=new Point[2];
if(p.length<NUM)
return bong(p);
//開始畫線了 求所有點在x坐標的中位數
int minX = (int) Double.POSITIVE_INFINITY;
int maxX = (int) Double.NEGATIVE_INFINITY;
for(int i = 0; i < p.length; i++){
if(p[i].getX() < minX)
minX = (int) p[i].getX();
if(p[i].getX() > maxX)
maxX = (int) p[i].getX();
}
int midX = (minX + maxX)/2;
//把以midx為界劃分出的點分成兩組放到兩個表
ArrayList<Point> L1=new ArrayList<Point>();
ArrayList<Point> L2=new ArrayList<Point>();
for(int i = 0; i < p.length; i++){
if(p[i].getX() <= midX)
L1.add(p[i]);
if(p[i].getX() > midX)
L2.add(p[i]);
}
//按x坐標排序
Point [] p1 = new Point[L1.size()];
Point [] p2 = new Point[L2.size()];
L1.toArray(p1);
L2.toArray(p2);
mergeSort(p1, "x"); //按X坐標升序排列
mergeSort(p2, "x"); //按X坐標升序排列
//遞歸求p1,p2中最近的兩個點
Point[] result1 = new Point[2];
result1 = findpair(p1);
Point[] result2 = new Point[2];
result2 = findpair(p2);
//求二者中的最小值
if (distance(result1[0], result1[1])<distance(result2[0], result2[1])) {
result=result1;
}else {
result=result2;
}
double distance=Math.min(distance(result1[0], result1[1]),distance(result2[0], result2[1]));
//開始劃分帶了 在兩個子集中找哪些距離劃分線小于d的保存
ArrayList<Point> L3 = new ArrayList<Point>();
for(int i = 0; i < p1.length; i++){
if(midX - p1[i].getX() < distance)
L3.add(p1[i]);
}
for(int i = 0; i < p2.length; i++){
if(p2[i].getX() - midX < distance){
L3.add(p2[i]);
}
}
//將得到的按照y升序排列
Point [] p3 = new Point [L3.size()];
L3.toArray(p3);
mergeSort(p3, "y");
//然后開始優化的窮舉 即比較之后的7個點
if (p3.length<NUM) {
Point[] temp= bong(p3);
if (distance(temp[0], temp[1])<distance&&distance(temp[0], temp[1])!=0) {
result=temp;
}
}else {
for(int i=0;i<p3.length-7;i++){
double tempd;
for(int j=1;j<8;j++){
if (i+j>=p3.length) {
break;
}else {
tempd=distance(p3[i], p3[i+j]);
if (tempd<distance&tempd!=0) {
result[0]=p3[i];
result[1]=p3[i+j];
}
}
}
}
}
return result;
}
//歸并排序
private static void mergeSort(Point[] p, String flag) {
Point[] result = new Point[p.length];
mergeSort(p, result, 0, p.length - 1, flag);
}
private static void mergeSort(Point[] a, Point [] result, int left, int right, String flag){
if(left < right){
int center = (left + right) >> 1;
//分治
mergeSort(a, result, left, center, flag);
mergeSort(a, result, center + 1, right, flag);
//合并
merge(a, result, left, center + 1, right, flag);
}
}
private static void merge(Point [] a, Point [] result, int leftPos, int rightPos, int rightEnd, String flag){
int leftEnd = rightPos - 1;
int numOfElements = rightEnd - leftPos + 1;
int tmpPos = leftPos; //游標變量, 另兩個游標變量分別是leftPos 和 rightPos
while(leftPos <= leftEnd && rightPos <= rightEnd){
if(flag.equals("x")){
if(a[leftPos].getX() <= a[rightPos].getX())
result[tmpPos++] = a[leftPos++];
else
result[tmpPos++] = a[rightPos++];
}else if(flag.equals("y")){
if(a[leftPos].getY() <= a[rightPos].getY())
result[tmpPos++] = a[leftPos++];
else
result[tmpPos++] = a[rightPos++];
}else
throw new RuntimeException();
}
while(leftPos <= leftEnd)
result[tmpPos++] = a[leftPos++];
while(rightPos <= rightEnd)
result[tmpPos++] = a[rightPos++];
//將排好序的段落拷貝到原數組中
System.arraycopy(result, rightEnd-numOfElements+1, a, rightEnd-numOfElements+1, numOfElements);
}
//窮舉法
private static Point[] bong(Point[] p){
Point[] result=new Point[2];
if (p.length<=1) {
result[0]=new Point(Double.MIN_VALUE, Double.MIN_VALUE);
result[1]=new Point(Double.MAX_VALUE, Double.MAX_VALUE);
return result;
}else{
double min=distance(p[0], p[1]);
int start=0;
int end=1;
for (int i = 0; i < p.length; i++) {
for (int j = i+1; j < p.length; j++) {
if (distance(p[i], p[j])<min&&distance(p[i], p[j])!=0) {
min=distance(p[i], p[j]);
start=i;
end=j;
}
}
}
result[0]=p[start];
result[1]=p[end];
return result;
}
}
//計算距離
private static double distance(Point p1,Point p2){
return Math.sqrt((p1.getX()-p2.getX())*(p1.getX()-p2.getX())+(p1.getY()-p2.getY())*(p1.getY()-p2.getY()));
}
//用一個類來表示點
static class Point implements Cloneable,Comparable<Point>{
double x,y;
public Point(double x, double y) {
super();
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public int compareTo(Point o) {
if(x == o.getX() && y == o.getY())
return 0;
else
return 1;
}
public boolean equals(Object p) {
// TODO 自動生成的方法存根
if (this.x==((Point) p).getX()&&this.y==((Point) p).getY()) {
return true;
}else {
return false;
}
}
}
}
我們采用了隨機數產生器隨機產生點,并統計了分治法與純粹暴力搜索的運行時間,差距相當明顯(這個程序當規模特別大的時候還是會棧溢出,為啥)
泊松分酒問題
講這道題純粹就是比較好玩,就記錄一下.泊松分酒是很著名的一道題,講的是假設某人有12品脫的啤酒一瓶,想從中倒出六品脫,但是恰巧身邊沒有6品脫的容器,僅有一個8品脫和一個5品脫的容器,怎樣倒才能將啤酒分為兩個6品脫呢?
我們用代碼模擬很簡單的就得到了答案
package com.fredal.structure; import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
public class Oil {
static class Status{
static int[] full={12,8,5};//滿的狀態
int[] bottle=new int[3];//瓶子的狀態
Status from;//從哪個狀態來的
public Status(int a,int b,int c){
bottle[0]=a;
bottle[1]=b;
bottle[2]=c;
}
//獲取某種狀態開始下一步的所有的狀態
public Set opreation(){
Set res=new HashSet();
//開始倒酒
for(int i=0;i<bottle.length;i++){
for(int j=0;j<bottle.length;j++){
if(i==j) continue; //不倒自己
if(bottle[i]==0) continue;//自己是空的 不倒
if(bottle[j]==full[j]) continue;//對方是滿的 不倒
Status t=new Status(bottle[0], bottle[1], bottle[2]);
t.from=this;//從自己這個狀態開始變化
//真的開始倒酒了 t.bottle[j]+=t.bottle[i];
t.bottle[i]=0;
if(t.bottle[j]>full[j]){//裝不下了
t.bottle[i]=t.bottle[j]-full[j];//滿的倒回去
t.bottle[j]=full[j];
}
res.add(t);
}
}
return res;
}
//是否含有某種狀態
public boolean has2(int x){
int index=0;
if (bottle[0]==x) index++;
if (bottle[1]==x) index++;
if (bottle[2]==x) index++;
return index==2?true:false;
}
public Status getFrom() {
return from;
}
public String toString(){
return "<" + bottle[0] + "," + bottle[1] + "," + bottle[2] + ">";
}
public int hashCode() {
return 100;
}
public boolean equals(Object obj) {
Status x=(Status)obj;
return bottle[0]==x.bottle[0]&&bottle[1]==x.bottle[1]&&bottle[2]==x.bottle[2];
}
}
public static void main(String[] args) {
Set<Status> all=new HashSet<Status>();//存放所有結果狀態
all.add(new Status(12, 0, 0));
for(;;){
Set newset=new HashSet();
for(Status x:all){//所有上一種狀態產生所有下一種狀態
Set t = x.opreation();
newset.addAll(t);
}
if(all.containsAll(newset)) break;//出口
all.addAll(newset);
}
LinkedList<Status> list=new LinkedList<Status>();//存放有6的一溜
for(Status k:all){
if(k.has2(6)){
while(k!=null){
list.push(k);
k=k.getFrom();//從終止狀態開始往上追溯
}
}
}
//輸出
while(!list.isEmpty()){
System.out.println(list.pop());
}
}
}
這個解法找到的其實是最優解,至于為什么呢,其實利用set的方法十分巧妙,結果集set里隨著一次次的分酒一次次地擴增,當第一次出現含有兩個6的狀態的時候,再往前追溯,步驟是最少的!因為這個我們想要的狀態是第一次出現.
假如我們每次都打印出all集合,可以知道,當第一次找到含有兩個6狀態的時候程序并沒有結束,因為還沒有找到所有的狀態.
而后面的狀態再進行分酒時,仍有可能產生兩個6的狀態,但是想要加入set集合的時候就行不通了,所以此程序只輸出最早加入的那一個解,并且是最優的.
當然這種算法并不能輸出所有的解,如果要得到所有的解,我們可以采用以下算法,這種算法借鑒了圖的深度搜索(DFS)以及回溯的技巧,需要注意的是,和8皇后問題一樣,需要回溯的時機有兩個,出錯的時候和找到某一組解的時候.
package com.fredal.structure;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Oil {
int[] full = new int[3]; //滿狀態 容量
int[] bottle = new int[3]; //瓶子的狀態
int target = 0; //目標
List<int[]> res = new ArrayList<int[]>();//存放結果
public void opreation(int[] bottle) {
for(int i=0;i<3;i++) {
for(int j=1;j<3;j++){//每個瓶子都不往自己倒 總共6種可能性
int[] temp = bottle.clone();//每次循環都創建臨時數組
int to=(i+j)%3;//(i+j)%3 是除每種i瓶子外其他兩個瓶子的序號,即要倒的目標
if(temp[i]==0) continue;//自己是空的 不倒
if(temp[to]==full[to]) continue;//對方是滿的 不倒
//開始倒酒
temp[to]+=temp[i];
temp[i]=0;
if(temp[to]>full[to]){//裝不下了
temp[i]=temp[to]-full[to];//滿出來的部分倒回去
temp[to]=full[to];
}
if(had(temp)) continue;//檢測是否已經存在相同狀態,防止重復
res.add(temp);//添加到結果鏈表
if(has2(temp)) return;//如果找到有兩個想要的狀態的結果就返回
opreation(temp);//繼續下一次分酒
res.remove(res.size()-1); //回溯 仔細體會
}
}
}
//是否以及含有狀態
private boolean had(int[] bottlex) {
for(int[] e:res)
if(e[0]==bottlex[0]&&e[1]==bottlex[1]&&e[2]==bottlex[2]) return true;
return false;
}
//檢測找到結果
private boolean has2(int[] bottle) {
int index=0;
for(int i=0;i<bottle.length;i++)
if(bottle[i]==target) index++;
if(index==2){
show(res);//輸出
res.remove(res.size()-1);//回溯
return true;
}
return false;
}
//打印
private void show(List<int[]> res) {
for(int[] e:res) {
System.out.println(e[0] + "," + e[1] + "," + e[2]);
}
System.out.println();
}
public static void main(String[] args) {
Oil o = new Oil();
Scanner scanner = new Scanner(System.in);
String s ="";
if(scanner.hasNext()) {
s = scanner.nextLine();
}
String[] data = s.split(",");
int[] d = new int[data.length];
for(int i=0;i<data.length;i++){
d[i] = Integer.parseInt(data[i]);
}
o.full = new int[]{d[0],d[1],d[2]};
o.bottle = new int[]{d[3],d[4],d[5]};
o.target = d[6];
o.res.add(new int[]{d[3],d[4],d[5]});//添加初始狀態
o.opreation(o.bottle);
}
}
顯然,按照深度搜索并不能有效地找到最優解.上面兩種算法都是比較巧的,我也比較喜歡.
如果要同時找到所有解和最優解,用圖的廣度搜索(BFS)會很方便,這也是網上采用的最多的,代碼到處都有,就不寫了.
更多內容與相關下載請查看擴展閱讀